Skip to content

Commit

Permalink
Change the chats flow to Nexmo
Browse files Browse the repository at this point in the history
  • Loading branch information
therealphildini committed Aug 15, 2017
1 parent a88b885 commit bf73c71
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 165 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ before_script:
after_success:
- codecov
install: pip install -r requirements.txt
script: coverage run manage.py test contacts profiles invitations --settings=logtacts.test_settings && coverage report --include='./*'
script: coverage run manage.py test chats contacts profiles payments invitations --settings=logtacts.test_settings && coverage report --include='./*'
deploy:
provider: heroku
api_key:
Expand Down
29 changes: 28 additions & 1 deletion chats/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
from django.core.cache import cache
from django.test import TestCase
from unittest.mock import MagicMock, patch

# Create your tests here.
from contacts import factories as contact_factories
from profiles import factories as profile_factories


from .views import nexmo_send, handle, help_message


class ChatFlowHandlerTests(TestCase):

def setUp(self):
self.sender = '18318675309'
self.receiver = '15105555555'
cache.delete(self.sender)
self.book = contact_factories.BookFactory.create()

def test_start_unknown_number(self):
with patch('chats.views.nexmo_send', return_value=None) as send_function:
handle(self.sender, self.receiver, 'Hello')
send_function.assert_called_with(self.receiver, self.sender, "Hmm... I can't find an account with this number. Do you have a ContactOtter account?")

def test_start_known_number(self):
profile = profile_factories.ProfileFactory.create(phone_number='+' + self.sender)
contact_factories.BookOwnerFactory.create(user=profile.user, book=self.book)
with patch('chats.views.nexmo_send', return_value=None) as send_function:
handle(self.sender, self.receiver, 'Hello')
send_function.assert_called_with(self.receiver, self.sender, help_message())
270 changes: 107 additions & 163 deletions chats/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
import time
import nexmo
from string import ascii_lowercase

from twilio.twiml import Response

from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.cache import cache
Expand All @@ -13,7 +13,6 @@
HttpResponseRedirect,
)
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View

Expand All @@ -33,161 +32,114 @@
GET_EMAIL = 'get_email'
NEW_ACCOUNT = 'new_account'


@csrf_exempt
def sms(request):
if request.method == 'GET':
return HttpResponseRedirect('/')
if request.method == 'POST':
overall_start = time.time()
cache_key = request.POST.get('From')
co_number = request.POST.get('To')
message = request.POST.get('Body').strip()
if not cache_key:
raise Http404()
flow_state = cache.get(cache_key)
if flow_state:
if message.lower() == 'done':
cache.delete(cache_key)
return help_message()
if flow_state == QUERY_ACCOUNT:
if message.lower() in ('yes', 'yep'):
cache.set(cache_key, GET_EMAIL, CACHE_TIMEOUT)
return create_message(
"Ok, what's the email address on your account?",
to=cache_key, sender=co_number,
)
else:
cache.delete(cache_key)
return create_message(
"Ok! Please go to https://www.contactotter.com to create an account.",
to=cache_key, sender=co_number,
)
if flow_state == GET_EMAIL:
try:
# TODO: Send a confirmation email for connecting phone
user = User.objects.get(email=message.lower())
profile, _ = Profile.objects.get_or_create(user=user)
profile.phone_number = cache_key
profile.save()
cache.delete(cache_key)
return create_message(
"Ok! Your phone is connected to your account.",
to=cache_key, sender=co_number,
)
except User.DoesNotExist:
cache.delete(cache_key)
return create_message(
"We couldn't find an account for that email. Please go to https://www.contactotter.com to create one",
to=cache_key, sender=co_number,
)
user, book = get_user_objects_from_message(request.POST)
if not user or not book:
cache.set(cache_key, QUERY_ACCOUNT, CACHE_TIMEOUT)
return create_message(
"Hmm... I can't find an account with this number. Do you have a ContactOtter account?",
to=cache_key, sender=co_number,
)

if flow_state:
if flow_state.startswith('log'):
name = ':'.join(flow_state.split(':')[1:])
contacts = SearchQuerySet().filter(book=book.id).filter(
SQ(name=AutoQuery(name)) | SQ(content=AutoQuery(name))
)
if len(message) == 1 and len(contacts) > 0:
index = ascii_lowercase.index(message.lower())
contact = contacts[index].object
cache.delete(cache_key)
return log_contact(contact, user)
cache.delete(cache_key)
return create_message(
"Sorry, I didn't understand that.",
to=cache_key, sender=co_number,
)
if flow_state.startswith('find'):
name = ':'.join(flow_state.split(':')[1:])
contacts = SearchQuerySet().filter(book=book.id).filter(
SQ(name=AutoQuery(name)) | SQ(content=AutoQuery(name))
)
if len(message) == 1 and len(contacts) > 0:
index = ascii_lowercase.index(message.lower())
contact = contacts[index].object
cache.delete(cache_key)
return create_message(
get_contact_string(contact), to=cache_key, sender=co_number,
)
cache.delete(cache_key)
return create_message(
"Sorry, I didn't understand that.",
to=cache_key, sender=co_number,
)


tokens = message.split(' ')
if len(tokens) < 2:
return help_message()

search_start = time.time()
if tokens[0].lower() in MET_PREFIXES:
if tokens[1].lower() == 'with':
del tokens[1]
name = ' '.join(tokens[1:])
client = nexmo.Client(key=settings.NEXMO_KEY, secret=settings.NEXMO_SECRET)

def nexmo_send(sender, receiver, message):
client.send_message({
'from': sender,
'to': receiver,
'text': message,
})
return HttpResponse()

def handle(sender, receiver, message):
overall_start = time.time()
flow_state = cache.get(sender)
if flow_state:
if message == 'done':
cache.delete(sender)
return nexmo_send(receiver, sender, help_message())
if flow_state == QUERY_ACCOUNT:
if message in ('yes', 'yep'):
cache.set(sender, GET_EMAIL, CACHE_TIMEOUT)
return nexmo_send(receiver, sender, "Ok, what's the email address on your account?")
else:
cache.delete(sender)
return nexmo_send(receiver, sender, "Ok! Please go to https://www.contactotter.com to create an account.")
user, book = get_user_objects_from_message(sender)
if not user or not book:
cache.set(sender, QUERY_ACCOUNT, CACHE_TIMEOUT)
return nexmo_send(receiver, sender, "Hmm... I can't find an account with this number. Do you have a ContactOtter account?")
if flow_state:
if flow_state.startswith('log'):
name = ':'.join(flow_state.split(':')[1:])
contacts = SearchQuerySet().filter(book=book.id).filter(
SQ(name=AutoQuery(name)) | SQ(content=AutoQuery(name))
)
if len(contacts) > 1:
cache.set(cache_key, "log:{}".format(name), CACHE_TIMEOUT)
response_string = "Which {} did you mean?\n".format(name)
response_string += get_string_from_search_contacts(contacts)
response_string += "(DONE to exit)"
return create_message(
response_string, to=cache_key, sender=co_number,
)
if len(contacts) == 1:
contact = contacts[0].object
else:
contact = Contact.objects.create(
book=book,
name=name,
)

cache.delete(cache_key)
return log_contact(contact, user)

if tokens[0].lower() == 'find':
name = ' '.join(tokens[1:])
if len(message) == 1 and len(contacts) > 0:
index = ascii_lowercase.index(message)
contact = contacts[index].object
cache.delete(sender)
log_contact(contact, user)
return nexmo_send(receiver, sender, "Updated {} ({})".format(contact.name, contact.get_complete_url()))
cache.delete(sender)
return nexmo_send(receiver, sender, "Sorry, I didn't understand that.")
if flow_state.startswith('find'):
name = ':'.join(flow_state.split(':')[1:])
contacts = SearchQuerySet().filter(book=book.id).filter(
SQ(name=AutoQuery(name)) | SQ(content=AutoQuery(name))
)
if len(contacts) == 0:
return create_message(
"Hmm... I didn't find any contacts.",
to=cache_key, sender=co_number,
)
if len(contacts) == 1:
return create_message(
get_contact_string(contacts[0].object),
to=cache_key, sender=co_number,
)
response_string = get_string_from_search_contacts(contacts)
if len(contacts) > 3:
response_string += "More: https://{}/search/?q={}".format(
Site.objects.get_current().domain,
name,
)
cache.set(cache_key, "find:{}".format(name), CACHE_TIMEOUT)
return create_message(
"Here's what I found for {}:\n{}".format(name, response_string),
to=cache_key, sender=co_number,
if len(message) == 1 and len(contacts) > 0:
index = ascii_lowercase.index(message)
contact = contacts[index].object
cache.delete(sender)
return nexmo_send(receiver, sender, get_contact_string(contact))
cache.delete(sender)
return nexmo_send(receiver, sender, "Sorry, I didn't understand that.")
tokens = message.split(' ')
if len(tokens) < 2:
return nexmo_send(receiver, sender, help_message())
search_start = time.time()
if tokens[0].lower() in MET_PREFIXES:
if tokens[1].lower() == 'with':
del tokens[1]
name = ' '.join(tokens[1:])
contacts = SearchQuerySet().filter(book=book.id).filter(
SQ(name=AutoQuery(name)) | SQ(content=AutoQuery(name))
)
if len(contacts) > 1:
cache.set(sender, "log:{}".format(name), CACHE_TIMEOUT)
response_string = "Which {} did you mean?\n".format(name)
response_string += get_string_from_search_contacts(contacts)
response_string += "(DONE to exit)"
return nexmo_send(receiver, sender, response_string)
if len(contacts) == 1:
contact = contacts[0].object
else:
contact = Contact.objects.create(
book=book,
name=name,
)
cache.delete(sender)
log_contact(contact, user)
return nexmo_send(receiver, sender, "Updated {} ({})".format(contact.name, contact.get_complete_url()))
if tokens[0].lower() == 'find':
name = ' '.join(tokens[1:])
contacts = SearchQuerySet().filter(book=book.id).filter(
SQ(name=AutoQuery(name)) | SQ(content=AutoQuery(name))
)
if len(contacts) == 0:
return nexmo_send(receiver, sender, "Hmm... I didn't find any contacts.")
if len(contacts) == 1:
return nexmo_send(receiver, sender, get_contact_string(contacts[0].object))
response_string = get_string_from_search_contacts(contacts)
if len(contacts) > 3:
response_string += "More: https://{}/search/?q={}".format(
Site.objects.get_current().domain,
name,
)
return help_message()

cache.set(sender, "find:{}".format(name), CACHE_TIMEOUT)
return nexmo_send(receiver, sender, "Here's what I found for {}:\n{}".format(name, response_string))
return nexmo_send(receiver, sender, help_message())

def create_message(message, to, sender):
r = Response()
r.message(message, to=to, sender=sender)
return HttpResponse(r.toxml(), content_type='text/xml')
@csrf_exempt
def sms(request):
if request.method == 'GET':
return handle(
request.GET.get('msisdn').strip('+'),
request.GET.get('to').strip('+'),
request.GET.get('text').strip().lower(),
)


def log_contact(contact, user):
Expand All @@ -200,11 +152,6 @@ def log_contact(contact, user):
)
contact.last_contact = time
contact.save()
r = Response()
r.message(
"Updated {} ({})".format(contact.name, contact.get_complete_url())
)
return HttpResponse(r.toxml(), content_type='text/xml')


def get_string_from_search_contacts(contacts):
Expand All @@ -230,27 +177,24 @@ def get_contact_string(contact):
return response_string


def get_user_objects_from_message(post_data):
sender = post_data.get('From')
def get_user_objects_from_message(sender):
sender = '+' + sender
try:
profile = Profile.objects.get(phone_number=sender)
user = profile.user
except Profile.DoesNotExist:
return None, None
try:
book = Book.objects.get_for_user(user)
except Book.DoesNotExist:
book = Book.objects.filter_for_user(user)[0]
except IndexError:
return None, None
return user, book


def help_message():
r = Response()
message_string = (
return (
"Hello! I understand:\n"
"'Add Jane Doe' to add Jane Doe as a contact\n"
"'Met Jane Doe' to log that you met Jane Doe\n"
"'Find Jane Doe' to search for Jane in your contacts"
)
r.message(message_string)
return HttpResponse(r.toxml(), content_type='text/xml')
4 changes: 4 additions & 0 deletions logtacts/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ def get_env_variable(var_name):
STRIPE_PUBLIC_KEY = get_env_variable('STRIPE_PUBLIC_KEY')
STRIPE_SECRET_KEY = get_env_variable('STRIPE_SECRET_KEY')

NEXMO_KEY = get_env_variable('NEXMO_KEY')
NEXMO_SECRET = get_env_variable('NEXMO_SECRET')
NEXMO_NUMBER = get_env_variable('NEXMO_NUMBER')

REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379')

CHANNEL_LAYERS = {
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ google-api-python-client==1.5.4
gunicorn==19.7.0
httplib2==0.10.3
msgpack-python==0.4.8
nexmo==1.5.0
nexus-yplan==1.5.0
oauth2client==4.0.0
oauthlib==2.0.1
Expand Down

0 comments on commit bf73c71

Please sign in to comment.