From 40f4adf78b7cfe3544f794c3472233fbf0bc8830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Fri, 31 May 2019 13:01:01 +0300 Subject: [PATCH] Add WhatsApp integration (uses Twilio same as the SMS integration) --- CHANGELOG.md | 1 + README.md | 1 + hc/api/models.py | 17 ++- hc/api/tests/test_notify.py | 50 ++++++++- hc/api/transports.py | 27 +++++ hc/front/forms.py | 2 + hc/front/tests/test_add_whatsapp.py | 70 ++++++++++++ hc/front/urls.py | 1 + hc/front/views.py | 35 ++++++ hc/settings.py | 3 +- static/img/integrations/whatsapp.png | Bin 0 -> 20780 bytes templates/front/channels.html | 29 ++++- templates/front/welcome.html | 9 ++ templates/integrations/add_whatsapp.html | 109 +++++++++++++++++++ templates/integrations/whatsapp_message.html | 7 ++ templates/payments/pricing.html | 4 +- 16 files changed, 357 insertions(+), 8 deletions(-) create mode 100644 hc/front/tests/test_add_whatsapp.py create mode 100644 static/img/integrations/whatsapp.png create mode 100644 templates/integrations/add_whatsapp.html create mode 100644 templates/integrations/whatsapp_message.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 38f167e2..132e7f8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Webhooks support HTTP PUT (#249) - Webhooks can use different req. bodies and headers for "up" and "down" events. (#249) - Show check's code instead of full URL on 992px - 1200px wide screens. (#253) +- Add WhatsApp integration (uses Twilio same as the SMS integration) ### Bug Fixes - Fix badges for tags containing special characters (#240, #237) diff --git a/README.md b/README.md index 6151fd76..25890c2d 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ Configurations settings loaded from environment variables: | TWILIO_ACCOUNT | `None` | TWILIO_AUTH | `None` | TWILIO_FROM | `None` +| TWILIO_USE_WHATSAPP | `"False"` | PD_VENDOR_KEY | `None` | TRELLO_APP_KEY | `None` | MATRIX_HOMESERVER | `None` diff --git a/hc/api/models.py b/hc/api/models.py index 900045c1..08daa68c 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -39,6 +39,7 @@ CHANNEL_KINDS = ( ("zendesk", "Zendesk"), ("trello", "Trello"), ("matrix", "Matrix"), + ("whatsapp", "WhatsApp"), ) PO_PRIORITIES = {-2: "lowest", -1: "low", 0: "normal", 1: "high", 2: "emergency"} @@ -328,6 +329,8 @@ class Channel(models.Model): return transports.Trello(self) elif self.kind == "matrix": return transports.Matrix(self) + elif self.kind == "whatsapp": + return transports.WhatsApp(self) else: raise NotImplementedError("Unknown channel kind: %s" % self.kind) @@ -495,7 +498,7 @@ class Channel(models.Model): @property def sms_number(self): - assert self.kind == "sms" + assert self.kind in ("sms", "whatsapp") if self.value.startswith("{"): doc = json.loads(self.value) return doc["value"] @@ -556,6 +559,18 @@ class Channel(models.Model): doc = json.loads(self.value) return doc.get("down") + @property + def whatsapp_notify_up(self): + assert self.kind == "whatsapp" + doc = json.loads(self.value) + return doc["up"] + + @property + def whatsapp_notify_down(self): + assert self.kind == "whatsapp" + doc = json.loads(self.value) + return doc["down"] + class Notification(models.Model): class Meta: diff --git a/hc/api/tests/test_notify.py b/hc/api/tests/test_notify.py index 6eac293a..0ce48d8d 100644 --- a/hc/api/tests/test_notify.py +++ b/hc/api/tests/test_notify.py @@ -522,7 +522,7 @@ class NotifyTestCase(BaseTestCase): mock_post.return_value.status_code = 200 self.channel.notify(self.check) - assert Notification.objects.count() == 1 + self.assertEqual(Notification.objects.count(), 1) args, kwargs = mock_post.call_args payload = kwargs["data"] @@ -575,3 +575,51 @@ class NotifyTestCase(BaseTestCase): self.channel.notify(self.check) self.assertTrue(mock_post.called) + + @patch("hc.api.transports.requests.request") + def test_whatsapp(self, mock_post): + definition = {"value": "+1234567890", "up": True, "down": True} + + self._setup_data("whatsapp", json.dumps(definition)) + self.check.last_ping = now() - td(hours=2) + + mock_post.return_value.status_code = 200 + + self.channel.notify(self.check) + self.assertEqual(Notification.objects.count(), 1) + + args, kwargs = mock_post.call_args + payload = kwargs["data"] + self.assertEqual(payload["To"], "whatsapp:+1234567890") + + # sent SMS counter should go up + self.profile.refresh_from_db() + self.assertEqual(self.profile.sms_sent, 1) + + @patch("hc.api.transports.requests.request") + def test_whatsapp_obeys_up_down_flags(self, mock_post): + definition = {"value": "+1234567890", "up": True, "down": False} + + self._setup_data("whatsapp", json.dumps(definition)) + self.check.last_ping = now() - td(hours=2) + + self.channel.notify(self.check) + self.assertEqual(Notification.objects.count(), 0) + + self.assertFalse(mock_post.called) + + @patch("hc.api.transports.requests.request") + def test_whatsapp_limit(self, mock_post): + # At limit already: + self.profile.last_sms_date = now() + self.profile.sms_sent = 50 + self.profile.save() + + definition = {"value": "+1234567890", "up": True, "down": True} + self._setup_data("whatsapp", json.dumps(definition)) + + self.channel.notify(self.check) + self.assertFalse(mock_post.called) + + n = Notification.objects.get() + self.assertTrue("Monthly message limit exceeded" in n.error) diff --git a/hc/api/transports.py b/hc/api/transports.py index f61b6cf0..07f92eac 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -415,6 +415,33 @@ class Sms(HttpTransport): return self.post(url, data=data, auth=auth) +class WhatsApp(HttpTransport): + URL = "https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json" + + def is_noop(self, check): + if check.status == "down": + return not self.channel.whatsapp_notify_down + else: + return not self.channel.whatsapp_notify_up + + def notify(self, check): + profile = Profile.objects.for_user(self.channel.project.owner) + if not profile.authorize_sms(): + return "Monthly message limit exceeded" + + url = self.URL % settings.TWILIO_ACCOUNT + auth = (settings.TWILIO_ACCOUNT, settings.TWILIO_AUTH) + text = tmpl("whatsapp_message.html", check=check, site_name=settings.SITE_NAME) + + data = { + "From": "whatsapp:%s" % settings.TWILIO_FROM, + "To": "whatsapp:%s" % self.channel.sms_number, + "Body": text, + } + + return self.post(url, data=data, auth=auth) + + class Trello(HttpTransport): URL = "https://api.trello.com/1/cards" diff --git a/hc/front/forms.py b/hc/front/forms.py index dda7af49..30494ade 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -133,6 +133,8 @@ class AddSmsForm(forms.Form): error_css_class = "has-error" label = forms.CharField(max_length=100, required=False) value = forms.CharField(max_length=16, validators=[phone_validator]) + down = forms.BooleanField(required=False, initial=True) + up = forms.BooleanField(required=False, initial=True) class ChannelNameForm(forms.Form): diff --git a/hc/front/tests/test_add_whatsapp.py b/hc/front/tests/test_add_whatsapp.py new file mode 100644 index 00000000..8c5dbd1f --- /dev/null +++ b/hc/front/tests/test_add_whatsapp.py @@ -0,0 +1,70 @@ +from django.test.utils import override_settings +from hc.api.models import Channel +from hc.test import BaseTestCase + +TEST_CREDENTIALS = { + "TWILIO_ACCOUNT": "foo", + "TWILIO_AUTH": "foo", + "TWILIO_FROM": "123", + "TWILIO_USE_WHATSAPP": True, +} + + +@override_settings(**TEST_CREDENTIALS) +class AddWhatsAppTestCase(BaseTestCase): + url = "/integrations/add_whatsapp/" + + 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 WhatsApp message") + + @override_settings(USE_PAYMENTS=True) + def test_it_warns_about_limits(self): + self.profile.sms_limit = 0 + self.profile.save() + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(self.url) + self.assertContains(r, "upgrade to a") + + def test_it_creates_channel(self): + form = { + "label": "My Phone", + "value": "+1234567890", + "down": "true", + "up": "true", + } + + 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, "whatsapp") + self.assertEqual(c.sms_number, "+1234567890") + self.assertEqual(c.name, "My Phone") + self.assertTrue(c.whatsapp_notify_down) + self.assertTrue(c.whatsapp_notify_up) + self.assertEqual(c.project, self.project) + + def test_it_obeys_up_down_flags(self): + form = {"label": "My Phone", "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, "whatsapp") + self.assertEqual(c.sms_number, "+1234567890") + self.assertEqual(c.name, "My Phone") + self.assertFalse(c.whatsapp_notify_down) + self.assertFalse(c.whatsapp_notify_up) + self.assertEqual(c.project, self.project) + + @override_settings(TWILIO_USE_WHATSAPP=False) + def test_it_obeys_use_whatsapp_flag(self): + self.client.login(username="alice@example.org", password="password") + r = self.client.get(self.url) + self.assertEqual(r.status_code, 404) diff --git a/hc/front/urls.py b/hc/front/urls.py index 4bb6b004..d3643d4f 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -39,6 +39,7 @@ channel_urls = [ path("telegram/bot/", views.telegram_bot, name="hc-telegram-webhook"), path("add_telegram/", views.add_telegram, name="hc-add-telegram"), path("add_sms/", views.add_sms, name="hc-add-sms"), + path("add_whatsapp/", views.add_whatsapp, name="hc-add-whatsapp"), path("add_trello/", views.add_trello, name="hc-add-trello"), path("add_trello/settings/", views.trello_settings, name="hc-trello-settings"), path("add_matrix/", views.add_matrix, name="hc-add-matrix"), diff --git a/hc/front/views.py b/hc/front/views.py index d9fa5837..8be9c81c 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -232,6 +232,7 @@ def index(request): "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, + "enable_whatsapp": settings.TWILIO_USE_WHATSAPP, "enable_pd": settings.PD_VENDOR_KEY is not None, "enable_trello": settings.TRELLO_APP_KEY is not None, "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None, @@ -603,6 +604,7 @@ def channels(request): "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, + "enable_whatsapp": settings.TWILIO_USE_WHATSAPP, "enable_pd": settings.PD_VENDOR_KEY is not None, "enable_trello": settings.TRELLO_APP_KEY is not None, "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None, @@ -1222,6 +1224,39 @@ def add_sms(request): return render(request, "integrations/add_sms.html", ctx) +@login_required +def add_whatsapp(request): + if not settings.TWILIO_USE_WHATSAPP: + raise Http404("whatsapp integration is not available") + + if request.method == "POST": + form = AddSmsForm(request.POST) + if form.is_valid(): + channel = Channel(project=request.project, kind="whatsapp") + channel.name = form.cleaned_data["label"] + channel.value = json.dumps( + { + "value": form.cleaned_data["value"], + "up": form.cleaned_data["up"], + "down": form.cleaned_data["down"], + } + ) + channel.save() + + channel.assign_all_checks() + return redirect("hc-channels") + else: + form = AddSmsForm() + + ctx = { + "page": "channels", + "project": request.project, + "form": form, + "profile": request.project.owner_profile, + } + return render(request, "integrations/add_whatsapp.html", ctx) + + @login_required def add_trello(request): if settings.TRELLO_APP_KEY is None: diff --git a/hc/settings.py b/hc/settings.py index 53f0e813..64946e59 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -187,10 +187,11 @@ PUSHBULLET_CLIENT_SECRET = os.getenv("PUSHBULLET_CLIENT_SECRET") TELEGRAM_BOT_NAME = os.getenv("TELEGRAM_BOT_NAME", "ExampleBot") TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") -# SMS (Twilio) integration +# SMS and WhatsApp (Twilio) integration TWILIO_ACCOUNT = os.getenv("TWILIO_ACCOUNT") TWILIO_AUTH = os.getenv("TWILIO_AUTH") TWILIO_FROM = os.getenv("TWILIO_FROM") +TWILIO_USE_WHATSAPP = envbool("TWILIO_USE_WHATSAPP", "False") # PagerDuty PD_VENDOR_KEY = os.getenv("PD_VENDOR_KEY") diff --git a/static/img/integrations/whatsapp.png b/static/img/integrations/whatsapp.png new file mode 100644 index 0000000000000000000000000000000000000000..74eb6b9519fcd49d93e6e70a51d995df91573cc3 GIT binary patch literal 20780 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 + {% if channels %} - {% if channels %} @@ -90,6 +90,14 @@ list {{ ch.trello_board_list|last }} {% elif ch.kind == "matrix" %} Matrix {{ ch.value }} + {% elif ch.kind == "whatsapp" %} + WhatsApp to {{ ch.sms_number }} + {% if ch.whatsapp_notify_down and not ch.whatsapp_notify_up %} + (down only) + {% endif %} + {% if ch.whatsapp_notify_up and not ch.whatsapp_notify_down %} + (up only) + {% endif %} {% else %} {{ ch.kind }} {% endif %} @@ -127,7 +135,7 @@ {% else %} Never {% endif %} - {% if ch.kind == "sms" %} + {% if ch.kind == "sms" or ch.kind == "whatsapp" %}

Used {{ profile.sms_sent_this_month }} of {{ profile.sms_limit }} sends this month.

{% endif %} @@ -156,8 +164,12 @@ {% endwith %} {% endfor %} - {% endif %}
Name, Details
+ {% else %} +
+ The project {{ project }} has no integrations set up yet. +
+ {% endif %}

Add More

@@ -312,6 +324,17 @@ Add Integration {% endif %} + {% if enable_whatsapp %} +
  • + WhatsApp icon + +

    WhatsApp {% if use_payments %}(paid plans){% endif %}

    +

    Get a WhatsApp message when a check goes up or down.

    + + Add Integration +
  • + {% endif %}
  • 10 Team Members
  • 1000 Log Entries per Check
  • API Access
  • -
  • 50 SMS Alerts per Month
  • +
  • 50 SMS & WhatsApp Alerts per Month
  • Email Support
  • {% if not request.user.is_authenticated %} @@ -139,7 +139,7 @@
  • Unlimited Team Members
  • 1000 Log Entries per Check
  • API Access
  • -
  • 500 SMS Alerts per Month
  • +
  • 500 SMS & WhatsApp Alerts per Month
  • Priority Email Support
  • {% if not request.user.is_authenticated %}