forked from GithubBackups/healthchecks
Discord integration
This commit is contained in:
parent
ff95f27827
commit
e5126925ec
@ -34,7 +34,8 @@ CHANNEL_KINDS = (("email", "Email"),
|
||||
("po", "Pushover"),
|
||||
("pushbullet", "Pushbullet"),
|
||||
("opsgenie", "OpsGenie"),
|
||||
("victorops", "VictorOps"))
|
||||
("victorops", "VictorOps"),
|
||||
("discord", "Discord"))
|
||||
|
||||
PO_PRIORITIES = {
|
||||
-2: "lowest",
|
||||
@ -243,6 +244,8 @@ class Channel(models.Model):
|
||||
return transports.Pushover(self)
|
||||
elif self.kind == "opsgenie":
|
||||
return transports.OpsGenie(self)
|
||||
elif self.kind == "discord":
|
||||
return transports.Discord(self)
|
||||
else:
|
||||
raise NotImplementedError("Unknown channel kind: %s" % self.kind)
|
||||
|
||||
@ -310,6 +313,24 @@ class Channel(models.Model):
|
||||
doc = json.loads(self.value)
|
||||
return doc["incoming_webhook"]["url"]
|
||||
|
||||
@property
|
||||
def discord_webhook_url(self):
|
||||
assert self.kind == "discord"
|
||||
if not self.value.startswith("{"):
|
||||
return self.value
|
||||
|
||||
doc = json.loads(self.value)
|
||||
return doc["webhook"]["url"]
|
||||
|
||||
@property
|
||||
def discord_webhook_id(self):
|
||||
assert self.kind == "discord"
|
||||
if not self.value.startswith("{"):
|
||||
return self.value
|
||||
|
||||
doc = json.loads(self.value)
|
||||
return doc["webhook"]["id"]
|
||||
|
||||
def latest_notification(self):
|
||||
return Notification.objects.filter(channel=self).latest()
|
||||
|
||||
|
@ -155,8 +155,8 @@ class NotifyTestCase(BaseTestCase):
|
||||
assert Notification.objects.count() == 1
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
json = kwargs["json"]
|
||||
self.assertEqual(json["event_type"], "trigger")
|
||||
payload = kwargs["json"]
|
||||
self.assertEqual(payload["event_type"], "trigger")
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_slack(self, mock_post):
|
||||
@ -167,8 +167,8 @@ class NotifyTestCase(BaseTestCase):
|
||||
assert Notification.objects.count() == 1
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
json = kwargs["json"]
|
||||
attachment = json["attachments"][0]
|
||||
payload = kwargs["json"]
|
||||
attachment = payload["attachments"][0]
|
||||
fields = {f["title"]: f["value"] for f in attachment["fields"]}
|
||||
self.assertEqual(fields["Last Ping"], "Never")
|
||||
|
||||
@ -213,8 +213,8 @@ class NotifyTestCase(BaseTestCase):
|
||||
self.assertEqual(n.error, "")
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
json = kwargs["json"]
|
||||
self.assertIn("DOWN", json["message"])
|
||||
payload = kwargs["json"]
|
||||
self.assertIn("DOWN", payload["message"])
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_opsgenie(self, mock_post):
|
||||
@ -226,8 +226,8 @@ class NotifyTestCase(BaseTestCase):
|
||||
self.assertEqual(n.error, "")
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
json = kwargs["json"]
|
||||
self.assertIn("DOWN", json["message"])
|
||||
payload = kwargs["json"]
|
||||
self.assertIn("DOWN", payload["message"])
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_pushover(self, mock_post):
|
||||
@ -238,8 +238,8 @@ class NotifyTestCase(BaseTestCase):
|
||||
assert Notification.objects.count() == 1
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
json = kwargs["data"]
|
||||
self.assertIn("DOWN", json["title"])
|
||||
payload = kwargs["data"]
|
||||
self.assertIn("DOWN", payload["title"])
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_victorops(self, mock_post):
|
||||
@ -250,5 +250,20 @@ class NotifyTestCase(BaseTestCase):
|
||||
assert Notification.objects.count() == 1
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
json = kwargs["json"]
|
||||
self.assertEqual(json["message_type"], "CRITICAL")
|
||||
payload = kwargs["json"]
|
||||
self.assertEqual(payload["message_type"], "CRITICAL")
|
||||
|
||||
@patch("hc.api.transports.requests.request")
|
||||
def test_discord(self, mock_post):
|
||||
v = json.dumps({"webhook": {"url": "123"}})
|
||||
self._setup_data("discord", v)
|
||||
mock_post.return_value.status_code = 200
|
||||
|
||||
self.channel.notify(self.check)
|
||||
assert Notification.objects.count() == 1
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
payload = kwargs["json"]
|
||||
attachment = payload["attachments"][0]
|
||||
fields = {f["title"]: f["value"] for f in attachment["fields"]}
|
||||
self.assertEqual(fields["Last Ping"], "Never")
|
||||
|
@ -237,3 +237,10 @@ class VictorOps(HttpTransport):
|
||||
}
|
||||
|
||||
return self.post(self.channel.value, payload)
|
||||
|
||||
|
||||
class Discord(HttpTransport):
|
||||
def notify(self, check):
|
||||
text = tmpl("slack_message.json", check=check)
|
||||
payload = json.loads(text)
|
||||
return self.post(self.channel.discord_webhook_url + "/slack", payload)
|
||||
|
46
hc/front/tests/test_add_discord.py
Normal file
46
hc/front/tests/test_add_discord.py
Normal file
@ -0,0 +1,46 @@
|
||||
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(DISCORD_CLIENT_ID="t1", DISCORD_CLIENT_SECRET="s1")
|
||||
class AddDiscordTestCase(BaseTestCase):
|
||||
url = "/integrations/add_discord/"
|
||||
|
||||
def test_instructions_work(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertContains(r, "Connect Discord", status_code=200)
|
||||
self.assertContains(r, "discordapp.com/api/oauth2/authorize")
|
||||
|
||||
@override_settings(DISCORD_CLIENT_ID=None)
|
||||
def test_it_requires_client_id(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
@patch("hc.front.views.requests.post")
|
||||
def test_it_handles_oauth_response(self, mock_post):
|
||||
oauth_response = {
|
||||
"access_token": "test-token",
|
||||
"webhook": {
|
||||
"url": "foo",
|
||||
"id": "bar"
|
||||
}
|
||||
}
|
||||
|
||||
mock_post.return_value.text = json.dumps(oauth_response)
|
||||
mock_post.return_value.json.return_value = oauth_response
|
||||
|
||||
url = self.url + "?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 Discord integration has been added!")
|
||||
|
||||
ch = Channel.objects.get()
|
||||
self.assertEqual(ch.discord_webhook_url, "foo")
|
@ -11,14 +11,10 @@ class AddPushbulletTestCase(BaseTestCase):
|
||||
url = "/integrations/add_pushbullet/"
|
||||
|
||||
def test_instructions_work(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertContains(r, "Connect Pushbullet")
|
||||
|
||||
def test_it_shows_instructions(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertContains(r, "www.pushbullet.com/authorize", status_code=200)
|
||||
self.assertContains(r, "Connect Pushbullet")
|
||||
|
||||
@override_settings(PUSHBULLET_CLIENT_ID=None)
|
||||
def test_it_requires_client_id(self):
|
||||
|
@ -19,6 +19,7 @@ channel_urls = [
|
||||
url(r'^add_slack_btn/$', views.add_slack_btn, name="hc-add-slack-btn"),
|
||||
url(r'^add_hipchat/$', views.add_hipchat, name="hc-add-hipchat"),
|
||||
url(r'^add_pushbullet/$', views.add_pushbullet, name="hc-add-pushbullet"),
|
||||
url(r'^add_discord/$', views.add_discord, name="hc-add-discord"),
|
||||
url(r'^add_pushover/$', views.add_pushover, name="hc-add-pushover"),
|
||||
url(r'^add_opsgenie/$', views.add_opsgenie, name="hc-add-opsgenie"),
|
||||
url(r'^add_victorops/$', views.add_victorops, name="hc-add-victorops"),
|
||||
|
@ -312,7 +312,8 @@ def channels(request):
|
||||
channel.checks = new_checks
|
||||
return redirect("hc-channels")
|
||||
|
||||
channels = Channel.objects.filter(user=request.team.user).order_by("created")
|
||||
channels = Channel.objects.filter(user=request.team.user)
|
||||
channels = channels.order_by("created")
|
||||
channels = channels.annotate(n_checks=Count("checks"))
|
||||
|
||||
num_checks = Check.objects.filter(user=request.team.user).count()
|
||||
@ -322,7 +323,8 @@ def channels(request):
|
||||
"channels": channels,
|
||||
"num_checks": num_checks,
|
||||
"enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
|
||||
"enable_pushover": settings.PUSHOVER_API_TOKEN is not None
|
||||
"enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
|
||||
"enable_discord": settings.DISCORD_CLIENT_ID is not None
|
||||
}
|
||||
return render(request, "front/channels.html", ctx)
|
||||
|
||||
@ -544,6 +546,53 @@ def add_pushbullet(request):
|
||||
return render(request, "integrations/add_pushbullet.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def add_discord(request):
|
||||
if settings.DISCORD_CLIENT_ID is None:
|
||||
raise Http404("discord integration is not available")
|
||||
|
||||
redirect_uri = settings.SITE_ROOT + reverse("hc-add-discord")
|
||||
if "code" in request.GET:
|
||||
code = request.GET.get("code", "")
|
||||
if len(code) < 8:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
result = requests.post("https://discordapp.com/api/oauth2/token", {
|
||||
"client_id": settings.DISCORD_CLIENT_ID,
|
||||
"client_secret": settings.DISCORD_CLIENT_SECRET,
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
"redirect_uri": redirect_uri
|
||||
})
|
||||
|
||||
doc = result.json()
|
||||
if "access_token" in doc:
|
||||
channel = Channel(kind="discord")
|
||||
channel.user = request.team.user
|
||||
channel.value = result.text
|
||||
channel.save()
|
||||
channel.assign_all_checks()
|
||||
messages.success(request,
|
||||
"The Discord integration has been added!")
|
||||
else:
|
||||
messages.warning(request, "Something went wrong")
|
||||
|
||||
return redirect("hc-channels")
|
||||
|
||||
authorize_url = "https://discordapp.com/api/oauth2/authorize?" + urlencode({
|
||||
"client_id": settings.DISCORD_CLIENT_ID,
|
||||
"scope": "webhook.incoming",
|
||||
"redirect_uri": redirect_uri,
|
||||
"response_type": "code"
|
||||
})
|
||||
|
||||
ctx = {
|
||||
"page": "channels",
|
||||
"authorize_url": authorize_url
|
||||
}
|
||||
return render(request, "integrations/add_discord.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def add_pushover(request):
|
||||
if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None:
|
||||
|
@ -136,6 +136,10 @@ COMPRESS_OFFLINE = True
|
||||
|
||||
EMAIL_BACKEND = "djmail.backends.default.EmailBackend"
|
||||
|
||||
# Discord integration -- override these in local_settings
|
||||
DISCORD_CLIENT_ID = None
|
||||
DISCORD_CLIENT_SECRET = None
|
||||
|
||||
# Slack integration -- override these in local_settings
|
||||
SLACK_CLIENT_ID = None
|
||||
SLACK_CLIENT_SECRET = None
|
||||
|
BIN
static/img/integrations/discord.png
Normal file
BIN
static/img/integrations/discord.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 720 B |
@ -27,15 +27,16 @@
|
||||
{% for ch in channels %}
|
||||
<tr class="channel-row">
|
||||
<td>
|
||||
{% if ch.kind == "email" %} Email {% endif %}
|
||||
{% if ch.kind == "webhook" %} Webhook {% endif %}
|
||||
{% if ch.kind == "slack" %} Slack {% endif %}
|
||||
{% if ch.kind == "hipchat" %} HipChat {% endif %}
|
||||
{% 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 == "opsgenie" %} OpsGenie {% endif %}
|
||||
{% if ch.kind == "email" %} Email
|
||||
{% elif ch.kind == "webhook" %} Webhook
|
||||
{% elif ch.kind == "slack" %} Slack
|
||||
{% elif ch.kind == "hipchat" %} HipChat
|
||||
{% elif ch.kind == "pd" %} PagerDuty
|
||||
{% elif ch.kind == "po" %} Pushover
|
||||
{% elif ch.kind == "victorops" %} VictorOps
|
||||
{% elif ch.kind == "pushbullet" %} Pushbullet
|
||||
{% elif ch.kind == "opsgenie" %} OpsGenie
|
||||
{% elif ch.kind == "discord" %} Discord {% endif %}
|
||||
</td>
|
||||
<td class="value-cell">
|
||||
{% if ch.kind == "email" %}
|
||||
@ -84,6 +85,8 @@
|
||||
{% elif ch.kind == "pushbullet" %}
|
||||
<span class="preposition">API key</span>
|
||||
{{ ch.value }}
|
||||
{% elif ch.kind == "discord" %}
|
||||
{{ ch.discord_webhook_id }}
|
||||
{% else %}
|
||||
{{ ch.value }}
|
||||
{% endif %}
|
||||
@ -214,6 +217,17 @@
|
||||
<a href="{% url 'hc-add-pushover' %}" class="btn btn-primary">Add Integration</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if enable_discord %}
|
||||
<li>
|
||||
<img src="{% static 'img/integrations/discord.png' %}"
|
||||
class="icon" alt="Discord icon" />
|
||||
|
||||
<h2>Discord</h2>
|
||||
<p>Cross-platform voice and text chat app designed for gamers.</p>
|
||||
|
||||
<a href="{% url 'hc-add-discord' %}" class="btn btn-primary">Add Integration</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="link-to-github">
|
||||
<img src="{% static 'img/integrations/missing.png' %}"
|
||||
class="icon" alt="Suggest New Integration" />
|
||||
|
@ -248,6 +248,13 @@
|
||||
<td>Notifications in <a href="https://slack.com/">Slack</a> channel.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<img width="22" height="22" alt="Discord icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsAQMAAAAkSshCAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAGUExURUxpcXKJ2kFuBesAAAABdFJOUwBA5thmAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAb0lEQVQY053QzQmAMAwF4CceRBAcoaPoSG5gQQfrKAUXKF68FGOf8feogfDd8pKgEJEI1AkBzAP7g+gnMqMkASD+or9xJ21DQnhg34BtRmVARZzv9mG932Nl+bruepCm8PYUxE8wLFWtFEquZArsBlghj/fhSNdMAAAAAElFTkSuQmCC" />
|
||||
</td>
|
||||
<td>Notifications in <a href="https://discordapp.com/">Discord</a> channel.</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<img width="22" height="22" alt="HipChat icon" src="data:image/jpeg;base64,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" />
|
||||
|
37
templates/integrations/add_discord.html
Normal file
37
templates/integrations/add_discord.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
{% load compress humanize staticfiles hc_extras %}
|
||||
|
||||
{% block title %}Add Discord - {% site_name %}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h1>Discord</h1>
|
||||
|
||||
<div class="jumbotron">
|
||||
<p>
|
||||
With this integration, {% site_name %} will send
|
||||
a notification to a <a href="http://discord.com/">Discord</a>
|
||||
channel when a check
|
||||
goes <strong>up</strong> or <strong>down</strong>.
|
||||
</p>
|
||||
|
||||
<form method="post" class="text-center">
|
||||
{% csrf_token %}
|
||||
<a href="{{ authorize_url }}" class="btn btn-default">
|
||||
<img class="ai-icon" src="{% static 'img/integrations/discord.png' %}" alt="Discord" />
|
||||
Connect Discord
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% compress js %}
|
||||
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user