diff --git a/hc/api/models.py b/hc/api/models.py index 5ce384f3..93c1b149 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -36,7 +36,8 @@ CHANNEL_KINDS = (("email", "Email"), ("opsgenie", "OpsGenie"), ("victorops", "VictorOps"), ("discord", "Discord"), - ("telegram", "Telegram")) + ("telegram", "Telegram"), + ("sms", "SMS")) PO_PRIORITIES = { -2: "lowest", @@ -270,6 +271,8 @@ class Channel(models.Model): return transports.Discord(self) elif self.kind == "telegram": return transports.Telegram(self) + elif self.kind == "sms": + return transports.Sms(self) else: raise NotImplementedError("Unknown channel kind: %s" % self.kind) diff --git a/hc/api/transports.py b/hc/api/transports.py index b32ef980..c6e06e6a 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -296,3 +296,24 @@ class Telegram(HttpTransport): def notify(self, check): text = tmpl("telegram_message.html", check=check) return self.send(self.channel.telegram_id, text) + + +class Sms(HttpTransport): + URL = 'https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json' + + def is_noop(self, check): + return check.status != "down" + + def notify(self, check): + url = self.URL % settings.TWILIO_ACCOUNT + auth = (settings.TWILIO_ACCOUNT, settings.TWILIO_AUTH) + text = tmpl("sms_message.html", check=check, + site_name=settings.SITE_NAME) + + data = { + 'From': settings.TWILIO_FROM, + 'To': self.channel.value, + 'Body': text, + } + + return self.post(url, data=data, auth=auth) diff --git a/hc/front/forms.py b/hc/front/forms.py index 3721adb1..79c5e3a4 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.core.validators import RegexValidator from hc.front.validators import (CronExpressionValidator, TimezoneValidator, WebhookValidator) @@ -64,3 +65,12 @@ class AddWebhookForm(forms.Form): def get_value(self): d = self.cleaned_data return "\n".join((d["value_down"], d["value_up"], d["post_data"])) + + +phone_validator = RegexValidator(regex='^\+\d{5,15}$', + message="Invalid phone number format.") + + +class AddSmsForm(forms.Form): + error_css_class = "has-error" + value = forms.CharField(max_length=16, validators=[phone_validator]) diff --git a/hc/front/tests/test_add_sms.py b/hc/front/tests/test_add_sms.py new file mode 100644 index 00000000..d92c7797 --- /dev/null +++ b/hc/front/tests/test_add_sms.py @@ -0,0 +1,46 @@ +from django.test.utils import override_settings +from hc.api.models import Channel +from hc.test import BaseTestCase + + +@override_settings(TWILIO_ACCOUNT="foo", TWILIO_AUTH="foo", TWILIO_FROM="123") +class AddSmsTestCase(BaseTestCase): + url = "/integrations/add_sms/" + + def test_instructions_work(self): + self.client.login(username="alice@example.org", password="password") + r = self.client.get(self.url) + self.assertContains(r, "Get a SMS message") + + def test_it_creates_channel(self): + form = {"value": "+1234567890"} + + self.client.login(username="alice@example.org", password="password") + r = self.client.post(self.url, form) + self.assertRedirects(r, "/integrations/") + + c = Channel.objects.get() + self.assertEqual(c.kind, "sms") + self.assertEqual(c.value, "+1234567890") + + def test_it_rejects_bad_number(self): + form = {"value": "not a phone number address"} + + self.client.login(username="alice@example.org", password="password") + r = self.client.post(self.url, form) + self.assertContains(r, "Invalid phone number format.") + + def test_it_trims_whitespace(self): + form = {"value": " +1234567890 "} + + self.client.login(username="alice@example.org", password="password") + self.client.post(self.url, form) + + c = Channel.objects.get() + self.assertEqual(c.value, "+1234567890") + + @override_settings(TWILIO_AUTH=None) + def test_it_requires_credentials(self): + self.client.login(username="alice@example.org", password="password") + r = self.client.get("/integrations/add_sms/") + self.assertEqual(r.status_code, 404) diff --git a/hc/front/urls.py b/hc/front/urls.py index a6ce7df1..23553286 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -26,6 +26,7 @@ channel_urls = [ url(r'^add_victorops/$', views.add_victorops, name="hc-add-victorops"), url(r'^telegram/bot/$', views.telegram_bot), url(r'^add_telegram/$', views.add_telegram, name="hc-add-telegram"), + url(r'^add_sms/$', views.add_sms, name="hc-add-sms"), url(r'^([\w-]+)/checks/$', views.channel_checks, name="hc-channel-checks"), url(r'^([\w-]+)/remove/$', views.remove_channel, name="hc-remove-channel"), url(r'^([\w-]+)/verify/([\w-]+)/$', views.verify_email, diff --git a/hc/front/views.py b/hc/front/views.py index e38ccf0c..1eef280c 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -25,7 +25,7 @@ from hc.api.models import (DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check, from hc.api.transports import Telegram from hc.front.forms import (AddWebhookForm, NameTagsForm, TimeoutForm, AddUrlForm, AddPdForm, AddEmailForm, - AddOpsGenieForm, CronForm) + AddOpsGenieForm, CronForm, AddSmsForm) from hc.front.schemas import telegram_callback from hc.lib import jsonschema from pytz import all_timezones @@ -106,6 +106,7 @@ def index(request): "enable_pushover": settings.PUSHOVER_API_TOKEN is not None, "enable_discord": settings.DISCORD_CLIENT_ID is not None, "enable_telegram": settings.TELEGRAM_TOKEN is not None, + "enable_sms": settings.TWILIO_AUTH is not None, "registration_open": settings.REGISTRATION_OPEN } @@ -350,7 +351,8 @@ def channels(request): "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None, "enable_pushover": settings.PUSHOVER_API_TOKEN is not None, "enable_discord": settings.DISCORD_CLIENT_ID is not None, - "enable_telegram": settings.TELEGRAM_TOKEN is not None + "enable_telegram": settings.TELEGRAM_TOKEN is not None, + "enable_sms": settings.TWILIO_AUTH is not None } return render(request, "front/channels.html", ctx) @@ -811,6 +813,27 @@ def add_telegram(request): return render(request, "integrations/add_telegram.html", ctx) +@login_required +def add_sms(request): + if settings.TWILIO_AUTH is None: + raise Http404("sms integration is not available") + + if request.method == "POST": + form = AddSmsForm(request.POST) + if form.is_valid(): + channel = Channel(user=request.team.user, kind="sms") + channel.value = form.cleaned_data["value"] + channel.save() + + channel.assign_all_checks() + return redirect("hc-channels") + else: + form = AddSmsForm() + + ctx = {"page": "channels", "form": form} + return render(request, "integrations/add_sms.html", ctx) + + def privacy(request): return render(request, "front/privacy.html", {}) diff --git a/hc/settings.py b/hc/settings.py index c2c49766..1cd71b5a 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -157,6 +157,11 @@ PUSHBULLET_CLIENT_SECRET = None TELEGRAM_BOT_NAME = "ExampleBot" TELEGRAM_TOKEN = None +# SMS (Twilio) integration -- override in local_settings.py +TWILIO_ACCOUNT = None +TWILIO_AUTH = None +TWILIO_FROM = None + if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")): from .local_settings import * else: diff --git a/static/img/integrations/sms.png b/static/img/integrations/sms.png new file mode 100644 index 00000000..8100074a Binary files /dev/null and b/static/img/integrations/sms.png differ diff --git a/templates/front/channels.html b/templates/front/channels.html index 604a162f..25cb0f4e 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -152,6 +152,17 @@ Add Integration + {% if enable_sms %} +
  • + SMS icon + +

    SMS

    +

    Get a text message to your phone when check goes down.

    + + Add Integration +
  • + {% endif %}
  • Webhook icon diff --git a/templates/front/welcome.html b/templates/front/welcome.html index 506ab88f..b8657c22 100644 --- a/templates/front/welcome.html +++ b/templates/front/welcome.html @@ -236,6 +236,15 @@ Good old email messages. + {% if enable_sms %} + + + Email icon + + SMS text messages. + + {% endif %} + Webhook icon diff --git a/templates/integrations/add_discord.html b/templates/integrations/add_discord.html index 92d5f1bb..0e15524b 100644 --- a/templates/integrations/add_discord.html +++ b/templates/integrations/add_discord.html @@ -28,10 +28,3 @@ {% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} -{% endblock %} diff --git a/templates/integrations/add_email.html b/templates/integrations/add_email.html index d181e9e9..4e2ff728 100644 --- a/templates/integrations/add_email.html +++ b/templates/integrations/add_email.html @@ -52,13 +52,4 @@ - - -{% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} {% endblock %} diff --git a/templates/integrations/add_hipchat.html b/templates/integrations/add_hipchat.html index 3f614edd..edb30b8e 100644 --- a/templates/integrations/add_hipchat.html +++ b/templates/integrations/add_hipchat.html @@ -92,13 +92,4 @@ - - -{% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} {% endblock %} diff --git a/templates/integrations/add_opsgenie.html b/templates/integrations/add_opsgenie.html index 8beef295..731a90dc 100644 --- a/templates/integrations/add_opsgenie.html +++ b/templates/integrations/add_opsgenie.html @@ -90,13 +90,4 @@ - - -{% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} {% endblock %} diff --git a/templates/integrations/add_pd.html b/templates/integrations/add_pd.html index 73d1f930..46e2b4e7 100644 --- a/templates/integrations/add_pd.html +++ b/templates/integrations/add_pd.html @@ -91,13 +91,4 @@ - - -{% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} {% endblock %} diff --git a/templates/integrations/add_sms.html b/templates/integrations/add_sms.html new file mode 100644 index 00000000..f8d44b86 --- /dev/null +++ b/templates/integrations/add_sms.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% load compress humanize staticfiles hc_extras %} + +{% block title %}Notification Channels - {% site_name %}{% endblock %} + + +{% block content %} +
    +
    +

    SMS

    + +

    Get a SMS message to your specified number when check goes down.

    + +

    Integration Settings

    + +
    + {% csrf_token %} +
    + +
    + + + {% if form.value.errors %} +
    + {{ form.value.errors|join:"" }} +
    + {% endif %} +
    +
    +
    +
    + +
    +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/templates/integrations/add_telegram.html b/templates/integrations/add_telegram.html index 836795e2..41451b37 100644 --- a/templates/integrations/add_telegram.html +++ b/templates/integrations/add_telegram.html @@ -89,16 +89,7 @@ src="{% static 'img/integrations/setup_telegram_3.png' %}"> - - {% endif %} {% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} -{% endblock %} diff --git a/templates/integrations/add_victorops.html b/templates/integrations/add_victorops.html index cd485853..753323a7 100644 --- a/templates/integrations/add_victorops.html +++ b/templates/integrations/add_victorops.html @@ -105,13 +105,4 @@ - - -{% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} {% endblock %} diff --git a/templates/integrations/add_webhook.html b/templates/integrations/add_webhook.html index 4ef91b01..1f388b34 100644 --- a/templates/integrations/add_webhook.html +++ b/templates/integrations/add_webhook.html @@ -113,13 +113,4 @@ - - -{% endblock %} - -{% block scripts %} -{% compress js %} - - -{% endcompress %} {% endblock %} diff --git a/templates/integrations/sms_message.html b/templates/integrations/sms_message.html new file mode 100644 index 00000000..1fdb6c4c --- /dev/null +++ b/templates/integrations/sms_message.html @@ -0,0 +1 @@ +{% load humanize %}{{ site_name }}: The check "{{ check.name_then_code }}" is DOWN. Last ping was {{ check.last_ping|naturaltime }}. \ No newline at end of file