Browse Source

Merge pull request #272 from caronc/master

Apprise Integration
pull/287/head
Pēteris Caune 5 years ago
committed by GitHub
parent
commit
ba886e90cb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 225 additions and 3 deletions
  1. +1
    -1
      .travis.yml
  2. +11
    -0
      README.md
  3. +3
    -0
      hc/api/models.py
  4. +40
    -1
      hc/api/tests/test_notify.py
  5. +26
    -1
      hc/api/transports.py
  6. +5
    -0
      hc/front/forms.py
  7. +29
    -0
      hc/front/tests/test_add_apprise.py
  8. +1
    -0
      hc/front/urls.py
  9. +29
    -0
      hc/front/views.py
  10. +4
    -0
      hc/settings.py
  11. BIN
      static/img/integrations/apprise.png
  12. +13
    -0
      templates/front/channels.html
  13. +8
    -0
      templates/front/welcome.html
  14. +49
    -0
      templates/integrations/add_apprise.html
  15. +5
    -0
      templates/integrations/apprise_description.html
  16. +1
    -0
      templates/integrations/apprise_title.html

+ 1
- 1
.travis.yml View File

@ -6,7 +6,7 @@ python:
- "3.7" - "3.7"
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install braintree coveralls mock mysqlclient reportlab
- pip install braintree coveralls mock mysqlclient reportlab apprise
env: env:
- DB=sqlite - DB=sqlite
- DB=mysql - DB=mysql


+ 11
- 0
README.md View File

@ -133,6 +133,7 @@ Configurations settings loaded from environment variables:
| MATRIX_HOMESERVER | `None` | MATRIX_HOMESERVER | `None`
| MATRIX_USER_ID | `None` | MATRIX_USER_ID | `None`
| MATRIX_ACCESS_TOKEN | `None` | MATRIX_ACCESS_TOKEN | `None`
| APPRISE_ENABLED | `"False"`
Some useful settings keys to override are: Some useful settings keys to override are:
@ -336,3 +337,13 @@ where to forward channel messages by invoking Telegram's
For this to work, your `SITE_ROOT` needs to be correct and use "https://" For this to work, your `SITE_ROOT` needs to be correct and use "https://"
scheme. scheme.
### Apprise
To enable Apprise integration, you will need to:
* ensure you have apprise installed in your local environment:
```bash
pip install apprise
```
* enable the apprise functionality by setting the `APPRISE_ENABLED` environment variable.

+ 3
- 0
hc/api/models.py View File

@ -41,6 +41,7 @@ CHANNEL_KINDS = (
("trello", "Trello"), ("trello", "Trello"),
("matrix", "Matrix"), ("matrix", "Matrix"),
("whatsapp", "WhatsApp"), ("whatsapp", "WhatsApp"),
("apprise", "Apprise"),
) )
PO_PRIORITIES = {-2: "lowest", -1: "low", 0: "normal", 1: "high", 2: "emergency"} PO_PRIORITIES = {-2: "lowest", -1: "low", 0: "normal", 1: "high", 2: "emergency"}
@ -392,6 +393,8 @@ class Channel(models.Model):
return transports.Matrix(self) return transports.Matrix(self)
elif self.kind == "whatsapp": elif self.kind == "whatsapp":
return transports.WhatsApp(self) return transports.WhatsApp(self)
elif self.kind == "apprise":
return transports.Apprise(self)
else: else:
raise NotImplementedError("Unknown channel kind: %s" % self.kind) raise NotImplementedError("Unknown channel kind: %s" % self.kind)


+ 40
- 1
hc/api/tests/test_notify.py View File

@ -7,8 +7,9 @@ from django.core import mail
from django.utils.timezone import now from django.utils.timezone import now
from hc.api.models import Channel, Check, Notification from hc.api.models import Channel, Check, Notification
from hc.test import BaseTestCase from hc.test import BaseTestCase
from mock import patch
from mock import patch, Mock
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from django.test.utils import override_settings
class NotifyTestCase(BaseTestCase): class NotifyTestCase(BaseTestCase):
@ -636,3 +637,41 @@ class NotifyTestCase(BaseTestCase):
n = Notification.objects.get() n = Notification.objects.get()
self.assertTrue("Monthly message limit exceeded" in n.error) self.assertTrue("Monthly message limit exceeded" in n.error)
@patch("apprise.Apprise")
@override_settings(APPRISE_ENABLED=True)
def test_apprise_enabled(self, mock_apprise):
self._setup_data("apprise", "123")
mock_aobj = Mock()
mock_aobj.add.return_value = True
mock_aobj.notify.return_value = True
mock_apprise.return_value = mock_aobj
self.channel.notify(self.check)
self.assertEqual(Notification.objects.count(), 1)
self.check.status = "up"
self.assertEqual(Notification.objects.count(), 1)
@patch("apprise.Apprise")
@override_settings(APPRISE_ENABLED=False)
def test_apprise_disabled(self, mock_apprise):
self._setup_data("apprise", "123")
mock_aobj = Mock()
mock_aobj.add.return_value = True
mock_aobj.notify.return_value = True
mock_apprise.return_value = mock_aobj
self.channel.notify(self.check)
self.assertEqual(Notification.objects.count(), 1)
def test_not_implimented(self):
self._setup_data("webhook", "http://example")
self.channel.kind = "invalid"
try:
self.channel.notify(self.check)
# Code should not reach here
assert False
except NotImplementedError:
# We expect to be here
assert True

+ 26
- 1
hc/api/transports.py View File

@ -8,6 +8,12 @@ from urllib.parse import quote, urlencode
from hc.accounts.models import Profile from hc.accounts.models import Profile
from hc.lib import emails from hc.lib import emails
try:
import apprise
except ImportError:
# Enforce
settings.APPRISE_ENABLED = False
def tmpl(template_name, **ctx): def tmpl(template_name, **ctx):
template_path = "integrations/%s" % template_name template_path = "integrations/%s" % template_name
@ -273,7 +279,7 @@ class PagerTree(HttpTransport):
class PagerTeam(HttpTransport): class PagerTeam(HttpTransport):
def notify(self, check): def notify(self, check):
url = self.channel.value url = self.channel.value
headers = {"Conent-Type": "application/json"}
headers = {"Content-Type": "application/json"}
payload = { payload = {
"incident_key": str(check.code), "incident_key": str(check.code),
"event_type": "trigger" if check.status == "down" else "resolve", "event_type": "trigger" if check.status == "down" else "resolve",
@ -461,3 +467,22 @@ class Trello(HttpTransport):
} }
return self.post(self.URL, params=params) return self.post(self.URL, params=params)
class Apprise(HttpTransport):
def notify(self, check):
if not settings.APPRISE_ENABLED:
# Not supported and/or enabled
return "Apprise is disabled and/or not installed."
a = apprise.Apprise()
title = tmpl("apprise_title.html", check=check)
body = tmpl("apprise_description.html", check=check)
a.add(self.channel.value)
notify_type = apprise.NotifyType.SUCCESS \
if check.status == "up" else apprise.NotifyType.FAILURE
return "Failed" if not \
a.notify(body=body, title=title, notify_type=notify_type) else None

+ 5
- 0
hc/front/forms.py View File

@ -159,3 +159,8 @@ class AddMatrixForm(forms.Form):
self.cleaned_data["room_id"] = doc["room_id"] self.cleaned_data["room_id"] = doc["room_id"]
return v return v
class AddAppriseForm(forms.Form):
error_css_class = "has-error"
url = forms.CharField(max_length=512)

+ 29
- 0
hc/front/tests/test_add_apprise.py View File

@ -0,0 +1,29 @@
from hc.api.models import Channel
from hc.test import BaseTestCase
from django.test.utils import override_settings
@override_settings(APPRISE_ENABLED=True)
class AddAppriseTestCase(BaseTestCase):
def test_instructions_work(self):
self.client.login(username="[email protected]", password="password")
r = self.client.get("/integrations/add_apprise/")
self.assertContains(r, "Integration Settings", status_code=200)
def test_it_works(self):
form = {"url": "json://example.org"}
self.client.login(username="[email protected]", password="password")
r = self.client.post("/integrations/add_apprise/", form)
self.assertRedirects(r, "/integrations/")
c = Channel.objects.get()
self.assertEqual(c.kind, "apprise")
self.assertEqual(c.value, "json://example.org")
self.assertEqual(c.project, self.project)
@override_settings(APPRISE_ENABLED=False)
def test_it_requires_client_id(self):
self.client.login(username="[email protected]", password="password")
r = self.client.get("/integrations/add_apprise/")
self.assertEqual(r.status_code, 404)

+ 1
- 0
hc/front/urls.py View File

@ -43,6 +43,7 @@ channel_urls = [
path("add_trello/", views.add_trello, name="hc-add-trello"), path("add_trello/", views.add_trello, name="hc-add-trello"),
path("add_trello/settings/", views.trello_settings, name="hc-trello-settings"), path("add_trello/settings/", views.trello_settings, name="hc-trello-settings"),
path("add_matrix/", views.add_matrix, name="hc-add-matrix"), path("add_matrix/", views.add_matrix, name="hc-add-matrix"),
path("add_apprise/", views.add_apprise, name="hc-add-apprise"),
path("<uuid:code>/checks/", views.channel_checks, name="hc-channel-checks"), path("<uuid:code>/checks/", views.channel_checks, name="hc-channel-checks"),
path("<uuid:code>/name/", views.update_channel_name, name="hc-channel-name"), path("<uuid:code>/name/", views.update_channel_name, name="hc-channel-name"),
path("<uuid:code>/test/", views.send_test_notification, name="hc-channel-test"), path("<uuid:code>/test/", views.send_test_notification, name="hc-channel-test"),


+ 29
- 0
hc/front/views.py View File

@ -44,6 +44,7 @@ from hc.front.forms import (
ChannelNameForm, ChannelNameForm,
EmailSettingsForm, EmailSettingsForm,
AddMatrixForm, AddMatrixForm,
AddAppriseForm,
) )
from hc.front.schemas import telegram_callback from hc.front.schemas import telegram_callback
from hc.front.templatetags.hc_extras import num_down_title, down_title, sortchecks from hc.front.templatetags.hc_extras import num_down_title, down_title, sortchecks
@ -236,6 +237,7 @@ def index(request):
"enable_pd": settings.PD_VENDOR_KEY is not None, "enable_pd": settings.PD_VENDOR_KEY is not None,
"enable_trello": settings.TRELLO_APP_KEY is not None, "enable_trello": settings.TRELLO_APP_KEY is not None,
"enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None, "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
"enable_apprise": settings.APPRISE_ENABLED is True,
"registration_open": settings.REGISTRATION_OPEN, "registration_open": settings.REGISTRATION_OPEN,
} }
@ -610,6 +612,7 @@ def channels(request):
"enable_pd": settings.PD_VENDOR_KEY is not None, "enable_pd": settings.PD_VENDOR_KEY is not None,
"enable_trello": settings.TRELLO_APP_KEY is not None, "enable_trello": settings.TRELLO_APP_KEY is not None,
"enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None, "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
"enable_apprise": settings.APPRISE_ENABLED is True,
"use_payments": settings.USE_PAYMENTS, "use_payments": settings.USE_PAYMENTS,
} }
@ -1325,6 +1328,32 @@ def add_matrix(request):
return render(request, "integrations/add_matrix.html", ctx) return render(request, "integrations/add_matrix.html", ctx)
@login_required
def add_apprise(request):
if not settings.APPRISE_ENABLED:
raise Http404("apprise integration is not available")
if request.method == "POST":
form = AddAppriseForm(request.POST)
if form.is_valid():
channel = Channel(project=request.project, kind="apprise")
channel.value = form.cleaned_data["url"]
channel.save()
channel.assign_all_checks()
messages.success(request, "The Apprise integration has been added!")
return redirect("hc-channels")
else:
form = AddAppriseForm()
ctx = {
"page": "channels",
"project": request.project,
"form": form,
}
return render(request, "integrations/add_apprise.html", ctx)
@login_required @login_required
@require_POST @require_POST
def trello_settings(request): def trello_settings(request):


+ 4
- 0
hc/settings.py View File

@ -204,6 +204,10 @@ MATRIX_HOMESERVER = os.getenv("MATRIX_HOMESERVER")
MATRIX_USER_ID = os.getenv("MATRIX_USER_ID") MATRIX_USER_ID = os.getenv("MATRIX_USER_ID")
MATRIX_ACCESS_TOKEN = os.getenv("MATRIX_ACCESS_TOKEN") MATRIX_ACCESS_TOKEN = os.getenv("MATRIX_ACCESS_TOKEN")
# Apprise
APPRISE_ENABLED = envbool("APPRISE_ENABLED", "False")
if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")): if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")):
from .local_settings import * from .local_settings import *
else: else:


BIN
static/img/integrations/apprise.png View File

Before After
Width: 133  |  Height: 133  |  Size: 28 KiB

+ 13
- 0
templates/front/channels.html View File

@ -69,6 +69,8 @@
{% endif %} {% endif %}
{% elif ch.kind == "webhook" %} {% elif ch.kind == "webhook" %}
Webhook Webhook
{% elif ch.kind == "apprise" %}
Apprise
{% elif ch.kind == "pushbullet" %} {% elif ch.kind == "pushbullet" %}
Pushbullet Pushbullet
{% elif ch.kind == "discord" %} {% elif ch.kind == "discord" %}
@ -211,6 +213,17 @@
<a href="{% url 'hc-add-webhook' %}" class="btn btn-primary">Add Integration</a> <a href="{% url 'hc-add-webhook' %}" class="btn btn-primary">Add Integration</a>
</li> </li>
{% if enable_apprise %}
<li>
<img src="{% static 'img/integrations/apprise.png' %}"
class="icon" alt="Pushover icon" />
<h2>Apprise</h2>
<p>Receive instant push notifications using Apprise; see <a href="https://github.com/caronc/apprise#popular-notification-services" >all of the supported services here</a>.</p>
<a href="{% url 'hc-add-apprise' %}" class="btn btn-primary">Add Integration</a>
</li>
{% endif %}
{% if enable_pushover %} {% if enable_pushover %}
<li> <li>
<img src="{% static 'img/integrations/po.png' %}" <img src="{% static 'img/integrations/po.png' %}"


+ 8
- 0
templates/front/welcome.html View File

@ -432,6 +432,14 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if enable_apprise %}
<div class="col-md-2 col-sm-4 col-xs-6">
<div class="integration">
<img src="{% static 'img/integrations/apprise.png' %}" class="icon" alt="Apprise icon" />
<h3>Apprise<br><small>>Push Notifications</small></h3>
</div>
</div>
{% endif %}
</div> </div>
<div class="row tour-section"> <div class="row tour-section">


+ 49
- 0
templates/integrations/add_apprise.html View File

@ -0,0 +1,49 @@
{% extends "base.html" %}
{% load humanize static hc_extras %}
{% block title %}Add Apprise - {% site_name %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-12">
<h1>Apprise</h1>
<p>
Identify as many Apprise URLs as you wish. You can use a comma (,) to identify
more than on URL if you wish to.
For a detailed list of all supported Apprise Notification URLs simply
<a href="https://github.com/caronc/apprise#popular-notification-services" >click here</a>.
</p>
<h2>Integration Settings</h2>
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="form-group {{ form.room_id.css_classes }}">
<label for="url" class="col-sm-2 control-label">Apprise URL</label>
<div class="col-sm-6">
<input
id="url"
type="text"
class="form-control"
name="url"
value="{{ form.url.value|default:"" }}">
{% if form.url.errors %}
<div class="help-block">
{{ form.url.errors|join:"" }}
</div>
{% endif %}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary">Save Integration</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

+ 5
- 0
templates/integrations/apprise_description.html View File

@ -0,0 +1,5 @@
{% load humanize %}
{{ check.name_then_code }} is {{ check.status|upper }}.
{% if check.status == "down" %}
Last ping was {{ check.last_ping|naturaltime }}.
{% endif %}

+ 1
- 0
templates/integrations/apprise_title.html View File

@ -0,0 +1 @@
{{ check.name_then_code }} is {{ check.status|upper }}

Loading…
Cancel
Save