diff --git a/hc/api/models.py b/hc/api/models.py index 251306c7..6fd4679e 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -176,6 +176,8 @@ class Channel(models.Model): return transports.PagerDuty(self) elif self.kind == "victorops": return transports.VictorOps(self) + elif self.kind == "pushbullet": + return transports.Pushbullet(self) elif self.kind == "po": return transports.Pushover(self) else: diff --git a/hc/api/transports.py b/hc/api/transports.py index 644e7485..0dd5a72b 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -65,8 +65,12 @@ class HttpTransport(Transport): def request(self, method, url, **kwargs): try: options = dict(kwargs) + if "headers" not in options: + options["headers"] = {} + options["timeout"] = 5 - options["headers"] = {"User-Agent": "healthchecks.io"} + options["headers"]["User-Agent"] = "healthchecks.io" + r = requests.request(method, url, **options) if r.status_code not in (200, 201, 204): return "Received status code %d" % r.status_code @@ -79,8 +83,8 @@ class HttpTransport(Transport): def get(self, url): return self.request("get", url) - def post(self, url, json): - return self.request("post", url, json=json) + def post(self, url, json, **kwargs): + return self.request("post", url, json=json, **kwargs) def post_form(self, url, data): return self.request("post", url, data=data) @@ -154,6 +158,23 @@ class PagerDuty(HttpTransport): return self.post(self.URL, payload) +class Pushbullet(HttpTransport): + def notify(self, check): + text = tmpl("pushbullet_message.html", check=check) + url = "https://api.pushbullet.com/v2/pushes" + headers = { + "Access-Token": self.channel.value, + "Conent-Type": "application/json" + } + payload = { + "type": "note", + "title": "healthchecks.io", + "body": text + } + + return self.post(url, payload, headers=headers) + + class Pushover(HttpTransport): URL = "https://api.pushover.net/1/messages.json" @@ -161,7 +182,7 @@ class Pushover(HttpTransport): others = self.checks().filter(status="down").exclude(code=check.code) ctx = { "check": check, - "down_checks": others, + "down_checks": others, } text = tmpl("pushover_message.html", **ctx) title = tmpl("pushover_title.html", **ctx) diff --git a/hc/front/tests/test_add_channel.py b/hc/front/tests/test_add_channel.py index 854f0328..5bea3aae 100644 --- a/hc/front/tests/test_add_channel.py +++ b/hc/front/tests/test_add_channel.py @@ -150,3 +150,9 @@ class AddChannelTestCase(BaseTestCase): c = Channel.objects.get() self.assertEqual(c.value, "\nhttp://foo.com") + + @override_settings(PUSHBULLET_CLIENT_ID="foo") + def test_pushbullet_instructions_work(self): + self.client.login(username="alice@example.org", password="password") + r = self.client.get("/integrations/add_pushbullet/") + self.assertContains(r, "www.pushbullet.com/authorize", status_code=200) diff --git a/hc/front/tests/test_pushbullet_callback.py b/hc/front/tests/test_pushbullet_callback.py new file mode 100644 index 00000000..55831a44 --- /dev/null +++ b/hc/front/tests/test_pushbullet_callback.py @@ -0,0 +1,27 @@ +import json + +from django.test.utils import override_settings +from hc.api.models import Channel +from hc.test import BaseTestCase +from mock import patch + + +@override_settings(PUSHBULLET_CLIENT_ID="t1", PUSHBULLET_CLIENT_SECRET="s1") +class PushbulletCallbackTestCase(BaseTestCase): + + @patch("hc.front.views.requests.post") + def test_it_works(self, mock_post): + oauth_response = {"access_token": "test-token"} + + mock_post.return_value.text = json.dumps(oauth_response) + mock_post.return_value.json.return_value = oauth_response + + url = "/integrations/add_pushbullet/?code=12345678" + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(url, follow=True) + self.assertRedirects(r, "/integrations/") + self.assertContains(r, "The Pushbullet integration has been added!") + + ch = Channel.objects.get() + self.assertEqual(ch.value, "test-token") diff --git a/hc/front/tests/test_slack_callback.py b/hc/front/tests/test_slack_callback.py index abb7c1ae..db7016f0 100644 --- a/hc/front/tests/test_slack_callback.py +++ b/hc/front/tests/test_slack_callback.py @@ -1,12 +1,10 @@ import json -from django.test.utils import override_settings from hc.api.models import Channel from hc.test import BaseTestCase from mock import patch -@override_settings(PUSHOVER_API_TOKEN="token", PUSHOVER_SUBSCRIPTION_URL="url") class SlackCallbackTestCase(BaseTestCase): @patch("hc.front.views.requests.post") diff --git a/hc/front/urls.py b/hc/front/urls.py index 89599e13..7f0bd29a 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -23,6 +23,7 @@ urlpatterns = [ url(r'^integrations/add_slack/$', views.add_slack, name="hc-add-slack"), url(r'^integrations/add_slack_btn/$', views.add_slack_btn, name="hc-add-slack-btn"), url(r'^integrations/add_hipchat/$', views.add_hipchat, name="hc-add-hipchat"), + url(r'^integrations/add_pushbullet/$', views.add_pushbullet, name="hc-add-pushbullet"), url(r'^integrations/add_pushover/$', views.add_pushover, name="hc-add-pushover"), url(r'^integrations/add_victorops/$', views.add_victorops, name="hc-add-victorops"), url(r'^integrations/([\w-]+)/checks/$', views.channel_checks, name="hc-channel-checks"), diff --git a/hc/front/views.py b/hc/front/views.py index 2f877531..7bb76b89 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -270,6 +270,7 @@ def channels(request): "page": "channels", "channels": channels, "num_checks": num_checks, + "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None, "enable_pushover": settings.PUSHOVER_API_TOKEN is not None } return render(request, "front/channels.html", ctx) @@ -418,6 +419,48 @@ def add_hipchat(request): return render(request, "integrations/add_hipchat.html", ctx) +@login_required +def add_pushbullet(request): + if "code" in request.GET: + code = request.GET.get("code", "") + if len(code) < 8: + return HttpResponseBadRequest() + + result = requests.post("https://api.pushbullet.com/oauth2/token", { + "client_id": settings.PUSHBULLET_CLIENT_ID, + "client_secret": settings.PUSHBULLET_CLIENT_SECRET, + "code": code, + "grant_type": "authorization_code" + }) + + doc = result.json() + if "access_token" in doc: + channel = Channel(kind="pushbullet") + channel.user = request.team.user + channel.value = doc["access_token"] + channel.save() + channel.assign_all_checks() + messages.success(request, + "The Pushbullet integration has been added!") + else: + messages.warning(request, "Something went wrong") + + return redirect("hc-channels") + + redirect_uri = settings.SITE_ROOT + reverse("hc-add-pushbullet") + authorize_url = "https://www.pushbullet.com/authorize?" + urlencode({ + "client_id": settings.PUSHBULLET_CLIENT_ID, + "redirect_uri": redirect_uri, + "response_type": "code" + }) + + ctx = { + "page": "channels", + "authorize_url": authorize_url + } + return render(request, "integrations/add_pushbullet.html", ctx) + + @login_required def add_pushover(request): if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None: diff --git a/static/css/channels.css b/static/css/channels.css index f4eee091..5f78653d 100644 --- a/static/css/channels.css +++ b/static/css/channels.css @@ -131,14 +131,9 @@ table.channels-table > tbody > tr > th { line-height: 40px; } - -.ai-icon img { - width: 48px; - height: 48px; -} - .btn img.ai-icon { height: 1.4em; + margin-right: 2px; } .add-integration h2 { diff --git a/static/img/integrations/pushbullet.png b/static/img/integrations/pushbullet.png new file mode 100644 index 00000000..b48b1abc Binary files /dev/null and b/static/img/integrations/pushbullet.png differ diff --git a/static/img/integrations/pushover.png b/static/img/integrations/pushover.png index 73f37988..6d16ece2 100644 Binary files a/static/img/integrations/pushover.png and b/static/img/integrations/pushover.png differ diff --git a/static/img/integrations/slack.png b/static/img/integrations/slack.png index edf63438..72d2e00a 100644 Binary files a/static/img/integrations/slack.png and b/static/img/integrations/slack.png differ diff --git a/static/img/logo-512-green.png b/static/img/logo-512-green.png new file mode 100644 index 00000000..cd749c33 Binary files /dev/null and b/static/img/logo-512-green.png differ diff --git a/templates/front/channels.html b/templates/front/channels.html index 0e08ec4f..8873ea35 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -34,6 +34,7 @@ {% if ch.kind == "pd" %} PagerDuty {% endif %} {% if ch.kind == "po" %} Pushover {% endif %} {% if ch.kind == "victorops" %} VictorOps {% endif %} + {% if ch.kind == "pushbullet" %} Pushbullet {% endif %} {% if ch.kind == "email" %} @@ -76,6 +77,9 @@ {% endif %} + {% elif ch.kind == "pushbullet" %} + API key + {{ ch.value }} {% else %} {{ ch.value }} {% endif %} @@ -175,6 +179,17 @@ Add Integration + {% if enable_pushbullet %} +
  • + Pushbullet icon + +

    Pushbullet

    +

    Pushbullet connects your devices, making them feel like one.

    + + Add Integration +
  • + {% endif %} {% if enable_pushover %}
  • +
    +

    Pushbullet

    + +
    +

    + With this integration, healthchecks.io will send + a Pushbullet + notification when a check + goes up or down. +

    + +
    + {% csrf_token %} + + Pushbullet + Connect Pushbullet + +
    +
    +
    + +{% endblock %} + +{% block scripts %} +{% compress js %} + + +{% endcompress %} +{% endblock %} diff --git a/templates/integrations/pushbullet_message.html b/templates/integrations/pushbullet_message.html new file mode 100644 index 00000000..d1a2dcff --- /dev/null +++ b/templates/integrations/pushbullet_message.html @@ -0,0 +1,7 @@ +{% load humanize %} + +{% if check.status == "down" %} +The check "{{ check.name_then_code }}" is DOWN. Last ping was {{ check.last_ping|naturaltime }} +{% else %} +The check "{{ check.name_then_code }}" received a ping and is now UP. +{% endif %}