Skip to content
Snippets Groups Projects
Commit 89cadcca authored by Leo P. Singer's avatar Leo P. Singer
Browse files

Add voice/SMS notifications using Twilio

parent 49f21e2a
No related branches found
No related tags found
No related merge requests found
...@@ -9,6 +9,8 @@ import json ...@@ -9,6 +9,8 @@ import json
import logging import logging
from django_twilio.client import twilio_client
from utils import gpsToUtc from utils import gpsToUtc
from query import filter_for_labels from query import filter_for_labels
...@@ -24,6 +26,11 @@ if settings.USE_LVALERT_OVERSEER: ...@@ -24,6 +26,11 @@ if settings.USE_LVALERT_OVERSEER:
log = logging.getLogger('gracedb.alert') log = logging.getLogger('gracedb.alert')
def get_twilio_from():
for from_ in twilio_client.phone_numbers.iter():
return from_.phone_number
raise RuntimeError('Could not determine "from" Twilio phone number')
def issueAlert(event, location, event_url, serialized_object=None): def issueAlert(event, location, event_url, serialized_object=None):
issueXMPPAlert(event, location, serialized_object=serialized_object) issueXMPPAlert(event, location, serialized_object=serialized_object)
issueEmailAlert(event, event_url) issueEmailAlert(event, event_url)
...@@ -61,6 +68,7 @@ def issueAlertForLabel(event, label, doxmpp, serialized_event=None, event_url=No ...@@ -61,6 +68,7 @@ def issueAlertForLabel(event, label, doxmpp, serialized_event=None, event_url=No
issueXMPPAlert(event, "", "label", label, serialized_event) issueXMPPAlert(event, "", "label", label, serialized_event)
# Email # Email
profileRecips = [] profileRecips = []
phoneRecips = []
pipeline = event.pipeline pipeline = event.pipeline
# Triggers on given label matching pipeline OR with no pipeline (wildcard type) # Triggers on given label matching pipeline OR with no pipeline (wildcard type)
triggers = label.trigger_set.filter(pipelines=pipeline) triggers = label.trigger_set.filter(pipelines=pipeline)
...@@ -76,7 +84,10 @@ def issueAlertForLabel(event, label, doxmpp, serialized_event=None, event_url=No ...@@ -76,7 +84,10 @@ def issueAlertForLabel(event, label, doxmpp, serialized_event=None, event_url=No
continue continue
for recip in trigger.contacts.all(): for recip in trigger.contacts.all():
profileRecips.append(recip.email) if recip.email:
profileRecips.append(recip.email)
if recip.phone:
phoneRecips.append(recip.phone)
if event.search: if event.search:
subject = "[gracedb] %s / %s / %s / %s" % (label.name, event.pipeline.name, event.search.name, event.graceid()) subject = "[gracedb] %s / %s / %s / %s" % (label.name, event.pipeline.name, event.search.name, event.graceid())
...@@ -104,6 +115,35 @@ def issueAlertForLabel(event, label, doxmpp, serialized_event=None, event_url=No ...@@ -104,6 +115,35 @@ def issueAlertForLabel(event, label, doxmpp, serialized_event=None, event_url=No
email = EmailMessage(subject, message, fromaddress, toaddresses, bccaddresses) email = EmailMessage(subject, message, fromaddress, toaddresses, bccaddresses)
email.send() email.send()
# twiml_base_url is the URL of a TwiML Bin
# (https://support.twilio.com/hc/en-us/articles/230878368)
# with the following content:
#
# <?xml version="1.0" encoding="UTF-8"?>
# <Response>
# <Say>
# A {{pipeline}} event with Grace DB ID {{graceid}} was labelled with {{label_lower}}.
# </Say>
# <Sms>
# A {{pipeline}} event with GraceDB ID {{graceid}} was labelled with {{label}}
# https://gracedb-test.ligo.org/events/view/{{graceid}}
# </Sms>
# </Response>
twiml_base_url = 'https://handler.twilio.com/twiml/EH7a2cef360c90eec301c3bf325ce1790a'
twiml_url = '{0}?pipeline={1}&graceid={2}&label={3}&label_lower={4}'.format(
twiml_base_url, event.pipeline.name, event.graceid(), label.name, label.name.lower())
log.info('phoneRecips: %s' % phoneRecips)
from_ = get_twilio_from()
log.info('from_: %s' % from_)
for recip in phoneRecips:
log.info('in for loop')
try:
log.info('issueAlertForLabel: calling %s', recip)
twilio_client.calls.create(recip, from_, twiml_url, method='GET')
except:
log.exception('Failed to create call')
def issueEmailAlert(event, event_url): def issueEmailAlert(event, event_url):
...@@ -118,6 +158,7 @@ def issueEmailAlert(event, event_url): ...@@ -118,6 +158,7 @@ def issueEmailAlert(event, event_url):
fromaddress = settings.ALERT_TEST_EMAIL_FROM fromaddress = settings.ALERT_TEST_EMAIL_FROM
toaddresses = settings.ALERT_TEST_EMAIL_TO toaddresses = settings.ALERT_TEST_EMAIL_TO
bccaddresses = [] bccaddresses = []
twilio_recips = []
else: else:
fromaddress = settings.ALERT_EMAIL_FROM fromaddress = settings.ALERT_EMAIL_FROM
toaddresses = settings.ALERT_EMAIL_TO toaddresses = settings.ALERT_EMAIL_TO
...@@ -127,15 +168,22 @@ def issueEmailAlert(event, event_url): ...@@ -127,15 +168,22 @@ def issueEmailAlert(event, event_url):
# See: https://bugs.ligo.org/redmine/issues/2185 # See: https://bugs.ligo.org/redmine/issues/2185
#bccaddresses = settings.ALERT_EMAIL_BCC #bccaddresses = settings.ALERT_EMAIL_BCC
bccaddresses = [] bccaddresses = []
twilio_recips = []
pipeline = event.pipeline pipeline = event.pipeline
triggers = pipeline.trigger_set.filter(labels=None) triggers = pipeline.trigger_set.filter(labels=None)
for trigger in triggers: for trigger in triggers:
for recip in trigger.contacts.all(): for recip in trigger.contacts.all():
if not trigger.farThresh: if not trigger.farThresh:
bccaddresses.append(recip.email) if recip.email:
bccaddresses.append(recip.email)
if recip.phone:
twilio_recips.append(recip.phone)
else: else:
if event.far and event.far < trigger.farThresh: if event.far and event.far < trigger.farThresh:
bccaddresses.append(recip.email) if recip.email:
bccaddresses.append(recip.email)
if recip.phone:
twilio_recips.append(recip.phone)
subject = "[gracedb] %s event. ID: %s" % (event.pipeline.name, event.graceid()) subject = "[gracedb] %s event. ID: %s" % (event.pipeline.name, event.graceid())
message = """ message = """
New Event New Event
...@@ -161,6 +209,32 @@ Event Summary: ...@@ -161,6 +209,32 @@ Event Summary:
#send_mail(subject, message, fromaddress, toaddresses) #send_mail(subject, message, fromaddress, toaddresses)
# twiml_base_url is the URL of a TwiML Bin
# (https://support.twilio.com/hc/en-us/articles/230878368)
# with the following content:
#
# <?xml version="1.0" encoding="UTF-8"?>
# <Response>
# <Say>
# A {{pipeline}} event with Grace DB ID {{graceid}} was created.
# </Say>
# <Sms>
# A {{pipeline}} event with GraceDB ID {{graceid}} was created.
# https://gracedb-test.ligo.org/events/view/{{graceid}}
# </Sms>
# </Response>
twiml_base_url = 'https://handler.twilio.com/twiml/EHe8ac043be47528c50558954791fb11fe'
twiml_url = '{0}?pipeline={1}&graceid={2}'.format(
twiml_base_url, event.pipeline.name, event.graceid())
from_ = get_twilio_from()
for recip in twilio_recips:
try:
log.info('issueEmailAlert: calling %s', recip)
twilio_client.calls.create(recip, from_, twiml_url, method='GET')
except:
log.exception('Failed to create call')
def issueXMPPAlert(event, location, alert_type="new", description="", serialized_object=None): def issueXMPPAlert(event, location, alert_type="new", description="", serialized_object=None):
nodename = "%s_%s" % (event.group.name, event.pipeline.name) nodename = "%s_%s" % (event.group.name, event.pipeline.name)
......
...@@ -34,10 +34,11 @@ from django.utils import timezone ...@@ -34,10 +34,11 @@ from django.utils import timezone
import logging import logging
import pytz import pytz
logger = logging.getLogger('gracedb.view_logic')
def _createEventFromForm(request, form): def _createEventFromForm(request, form):
saved = False saved = False
warnings = [] warnings = []
logger = logging.getLogger(__name__)
try: try:
group = Group.objects.get(name=form.cleaned_data['group']) group = Group.objects.get(name=form.cleaned_data['group'])
pipeline = Pipeline.objects.get(name=form.cleaned_data['pipeline']) pipeline = Pipeline.objects.get(name=form.cleaned_data['pipeline'])
...@@ -176,11 +177,13 @@ def create_label(event, request, labelName, doAlert=True, doXMPP=True): ...@@ -176,11 +177,13 @@ def create_label(event, request, labelName, doAlert=True, doXMPP=True):
log.save() log.save()
except Exception as e: except Exception as e:
# XXX This looks a bit odd to me. # XXX This looks a bit odd to me.
logger.exception('Problem saving log message')
d['error'] = str(e) d['error'] = str(e)
try: try:
issueAlertForLabel(event, label, doXMPP, event_url=event_url) issueAlertForLabel(event, label, doXMPP, event_url=event_url)
except Exception, e: except Exception as e:
logger.exception('Problem saving log message')
d['warning'] = "Problem issuing alert (%s)" % str(e) d['warning'] = "Problem issuing alert (%s)" % str(e)
# XXX Strange return value. Just warnings. Can really be ignored, I think. # XXX Strange return value. Just warnings. Can really be ignored, I think.
return json.dumps(d) return json.dumps(d)
......
...@@ -337,6 +337,7 @@ INSTALLED_APPS = ( ...@@ -337,6 +337,7 @@ INSTALLED_APPS = (
'ligoauth', 'ligoauth',
'rest_framework', 'rest_framework',
'guardian', 'guardian',
'django_twilio',
) )
REST_FRAMEWORK = { REST_FRAMEWORK = {
......
...@@ -150,6 +150,7 @@ INSTALLED_APPS = ( ...@@ -150,6 +150,7 @@ INSTALLED_APPS = (
'rest_framework', 'rest_framework',
# 'south', # 'south',
'guardian', 'guardian',
'django_twilio',
) )
INTERNAL_IPS = ( INTERNAL_IPS = (
......
...@@ -87,6 +87,7 @@ INSTALLED_APPS = ( ...@@ -87,6 +87,7 @@ INSTALLED_APPS = (
'ligoauth', 'ligoauth',
'rest_framework', 'rest_framework',
'guardian', 'guardian',
'django_twilio',
#'debug_toolbar', #'debug_toolbar',
#'debug_panel', #'debug_panel',
) )
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<li> <li>
<!-- <a href="{% url "userprofile-edit-contact" contact.id %}">Edit</a> --> <!-- <a href="{% url "userprofile-edit-contact" contact.id %}">Edit</a> -->
<a href="{% url "userprofile-delete-contact" contact.id %}">Delete</a> <a href="{% url "userprofile-delete-contact" contact.id %}">Delete</a>
{{ contact.desc }} / {{ contact.email }} {{ contact.desc }} / {{ contact.email }} {{ contact.phone }}
</li> </li>
</ul> </ul>
{% endfor %} {% endfor %}
......
...@@ -3,7 +3,31 @@ from django.db import models ...@@ -3,7 +3,31 @@ from django.db import models
from gracedb.models import Label, Pipeline from gracedb.models import Label, Pipeline
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User from django.contrib.auth.models import User
import phonenumbers
def validate_phone(value):
try:
phone = phonenumbers.parse(value, 'US')
except phonenumbers.NumberParseException:
raise ValidationError('Not a valid phone number: {0}'.format(value))
if not phonenumbers.is_valid_number(phone):
raise ValidationError('Not a valid phone number: {0}'.format(value))
return phonenumbers.format_number(phone, phonenumbers.PhoneNumberFormat.E164)
class PhoneNumberField(models.CharField):
def __init__(self, *args, **kwargs):
validators = kwargs.get('validators', []) + [validate_phone]
kwargs = dict(kwargs, max_length=255, validators=validators)
super(PhoneNumberField, self).__init__(*args, **kwargs)
def get_prep_value(self, value):
if value:
return validate_phone(value)
else:
return ''
#class Notification(models.Model): #class Notification(models.Model):
# user = models.ForeignKey(User, null=False) # user = models.ForeignKey(User, null=False)
...@@ -16,7 +40,8 @@ class Contact(models.Model): ...@@ -16,7 +40,8 @@ class Contact(models.Model):
user = models.ForeignKey(User, null=False) user = models.ForeignKey(User, null=False)
#new_user = models.ForeignKey(DjangoUser, null=True) #new_user = models.ForeignKey(DjangoUser, null=True)
desc = models.CharField(max_length=20) desc = models.CharField(max_length=20)
email = models.EmailField() email = models.EmailField(blank=True)
phone = PhoneNumberField(blank=True)
def __unicode__(self): def __unicode__(self):
#return "%s: %s" % (self.user.name, self.desc) #return "%s: %s" % (self.user.name, self.desc)
......
...@@ -143,7 +143,8 @@ def createContact(request): ...@@ -143,7 +143,8 @@ def createContact(request):
c = Contact( c = Contact(
user=request.user, user=request.user,
desc = form.cleaned_data['desc'], desc = form.cleaned_data['desc'],
email = form.cleaned_data['email'] email = form.cleaned_data['email'],
phone = form.cleaned_data['phone']
) )
c.save() c.save()
request.session['flash_msg'] = "Created: %s" % c request.session['flash_msg'] = "Created: %s" % c
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment