diff --git a/hc/api/models.py b/hc/api/models.py index c9f29e69..5318254b 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -43,7 +43,8 @@ CHANNEL_KINDS = (("email", "Email"), ("telegram", "Telegram"), ("sms", "SMS"), ("zendesk", "Zendesk"), - ("trello", "Trello")) + ("trello", "Trello"), + ("matrix", "Matrix")) PO_PRIORITIES = { -2: "lowest", @@ -339,6 +340,8 @@ class Channel(models.Model): return transports.Sms(self) elif self.kind == "trello": return transports.Trello(self) + elif self.kind == "matrix": + return transports.Matrix(self) else: raise NotImplementedError("Unknown channel kind: %s" % self.kind) diff --git a/hc/api/transports.py b/hc/api/transports.py index ce39708c..b621d08d 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -3,7 +3,7 @@ from django.template.loader import render_to_string from django.utils import timezone import json import requests -from urllib.parse import quote +from urllib.parse import quote, urlencode from hc.accounts.models import Profile from hc.lib import emails @@ -343,6 +343,28 @@ class VictorOps(HttpTransport): return self.post(self.channel.value, json=payload) +class Matrix(HttpTransport): + def get_url(self): + s = quote(self.channel.value) + + url = settings.MATRIX_HOMESERVER + url += "/_matrix/client/r0/rooms/%s/send/m.room.message?" % s + url += urlencode({"access_token": settings.MATRIX_ACCESS_TOKEN}) + return url + + def notify(self, check): + plain = tmpl("matrix_description.html", check=check) + formatted = tmpl("matrix_description_formatted.html", check=check) + payload = { + "msgtype": "m.text", + "body": plain, + "format": "org.matrix.custom.html", + "formatted_body": formatted + } + + return self.post(self.get_url(), json=payload) + + class Discord(HttpTransport): def notify(self, check): text = tmpl("slack_message.json", check=check) diff --git a/hc/front/forms.py b/hc/front/forms.py index c82de82c..28a48890 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -1,11 +1,14 @@ from datetime import timedelta as td import json import re +from urllib.parse import quote, urlencode from django import forms +from django.conf import settings from django.core.validators import RegexValidator from hc.front.validators import (CronExpressionValidator, TimezoneValidator, WebhookValidator) +import requests class NameTagsForm(forms.Form): @@ -116,3 +119,24 @@ class AddSmsForm(forms.Form): class ChannelNameForm(forms.Form): name = forms.CharField(max_length=100, required=False) + + +class AddMatrixForm(forms.Form): + error_css_class = "has-error" + alias = forms.CharField(max_length=40) + + def clean_alias(self): + v = self.cleaned_data["alias"] + + # validate it by trying to join + url = settings.MATRIX_HOMESERVER + url += "/_matrix/client/r0/join/%s?" % quote(v) + url += urlencode({"access_token": settings.MATRIX_ACCESS_TOKEN}) + doc = requests.post(url, {}).json() + if "error" in doc: + raise forms.ValidationError( + "Response from Matrix: %s" % doc["error"]) + + self.cleaned_data["room_id"] = doc["room_id"] + + return v diff --git a/hc/front/urls.py b/hc/front/urls.py index 7c6b4d4e..f2ddf2a6 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -38,6 +38,7 @@ channel_urls = [ path('add_sms/', views.add_sms, name="hc-add-sms"), 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"), path('/checks/', views.channel_checks, name="hc-channel-checks"), path('/name/', views.update_channel_name, name="hc-channel-name"), path('/remove/', views.remove_channel, name="hc-remove-channel"), diff --git a/hc/front/views.py b/hc/front/views.py index 9a5f136b..2cd70a9f 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta as td import json -from urllib.parse import urlencode +from urllib.parse import urlencode, quote from croniter import croniter from django.conf import settings @@ -24,7 +24,7 @@ from hc.api.transports import Telegram from hc.front.forms import (AddWebhookForm, NameTagsForm, TimeoutForm, AddUrlForm, AddEmailForm, AddOpsGenieForm, CronForm, AddSmsForm, - ChannelNameForm, EmailSettingsForm) + ChannelNameForm, EmailSettingsForm, AddMatrixForm) from hc.front.schemas import telegram_callback from hc.front.templatetags.hc_extras import (num_down_title, down_title, sortchecks) @@ -550,6 +550,7 @@ def channels(request): "enable_sms": settings.TWILIO_AUTH is not None, "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, "use_payments": settings.USE_PAYMENTS } @@ -1144,6 +1145,37 @@ def add_trello(request): return render(request, "integrations/add_trello.html", ctx) +@login_required +def add_matrix(request): + if settings.MATRIX_ACCESS_TOKEN is None: + raise Http404("matrix integration is not available") + + if request.method == "POST": + form = AddMatrixForm(request.POST) + if form.is_valid(): + channel = Channel(project=request.project, kind="matrix") + channel.value = form.cleaned_data["room_id"] + + # If user supplied room alias instead of ID, use it as channel name + alias = form.cleaned_data["alias"] + if not alias.startswith("!"): + channel.name = alias + + channel.save() + + channel.assign_all_checks() + messages.success(request, "The Matrix integration has been added!") + return redirect("hc-channels") + else: + form = AddMatrixForm() + + ctx = { + "page": "channels", + "form": form + } + return render(request, "integrations/add_matrix.html", ctx) + + @login_required @require_POST def trello_settings(request): diff --git a/hc/settings.py b/hc/settings.py index 6f345746..144326cb 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -196,6 +196,10 @@ PD_VENDOR_KEY = os.getenv("PD_VENDOR_KEY") # Trello TRELLO_APP_KEY = os.getenv("TRELLO_APP_KEY") +# Matrix +MATRIX_HOMESERVER = os.getenv("MATRIX_HOMESERVER") +MATRIX_ACCESS_TOKEN = os.getenv("MATRIX_ACCESS_TOKEN") + if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")): from .local_settings import * else: diff --git a/static/img/integrations/matrix.png b/static/img/integrations/matrix.png new file mode 100644 index 00000000..a7ca9b0c Binary files /dev/null and b/static/img/integrations/matrix.png differ diff --git a/templates/front/channels.html b/templates/front/channels.html index b6b0cb11..ba50e0ab 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -80,6 +80,8 @@ Trello board {{ ch.trello_board_list|first }}, list {{ ch.trello_board_list|last }} + {% elif ch.kind == "matrix" %} + Matrix {{ ch.value }} {% else %} {{ ch.kind }} {% endif %} @@ -279,6 +281,17 @@ Add Integration {% endif %} + {% if enable_matrix %} +
  • + Matrix icon + +

    Matrix

    +

    Post notifications to a Matrix room.

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