From cd99af14ba00926721a6c3bd710f87cb7ec260b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Sat, 9 Jan 2021 11:58:18 +0200 Subject: [PATCH] Add Signal integration Fixes: #428 --- CHANGELOG.md | 1 + README.md | 35 ++++++++ hc/api/models.py | 17 +++- hc/api/tests/test_notify_email.py | 2 +- hc/api/tests/test_notify_signal.py | 82 +++++++++++++++++ hc/api/transports.py | 25 ++++++ hc/front/tests/test_add_signal.py | 60 +++++++++++++ hc/front/urls.py | 1 + hc/front/views.py | 34 +++++++ hc/settings.py | 4 + static/css/icomoon.css | 14 +-- static/fonts/icomoon.eot | Bin 13236 -> 14060 bytes static/fonts/icomoon.svg | 1 + static/fonts/icomoon.ttf | Bin 13072 -> 13896 bytes static/fonts/icomoon.woff | Bin 13148 -> 13972 bytes static/img/integrations/signal.png | Bin 0 -> 2182 bytes templates/front/channels.html | 19 ++++ templates/front/event_summary.html | 5 ++ templates/front/welcome.html | 12 +++ templates/integrations/add_signal.html | 98 +++++++++++++++++++++ templates/integrations/signal_message.html | 7 ++ 21 files changed, 410 insertions(+), 7 deletions(-) create mode 100644 hc/api/tests/test_notify_signal.py create mode 100644 hc/front/tests/test_add_signal.py create mode 100644 static/img/integrations/signal.png create mode 100644 templates/integrations/add_signal.html create mode 100644 templates/integrations/signal_message.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 020a58d8..706bef88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - Update OpsGenie instructions (#450) - Update the email notification template to include more check and last ping details - Improve the crontab snippet in the "Check Details" page (#465) +- Add Signal integration (#428) ## v1.18.0 - 2020-12-09 diff --git a/README.md b/README.md index 4752e091..3009c9f1 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,8 @@ Healthchecks reads configuration from the following environment variables: | PUSHOVER_SUBSCRIPTION_URL | `None` | REMOTE_USER_HEADER | `None` | See [External Authentication](#external-authentication) for details. | SHELL_ENABLED | `"False"` +| SIGNAL_CLI_USERNAME | `None` +| SIGNAL_CLI_CMD | `signal-cli` | Path to the signal-cli executable | SLACK_CLIENT_ID | `None` | SLACK_CLIENT_SECRET | `None` | TELEGRAM_BOT_NAME | `"ExampleBot"` @@ -407,6 +409,39 @@ To enable the Pushover integration, you will need to: variables. The Pushover subscription URL should look similar to `https://pushover.net/subscribe/yourAppName-randomAlphaNumericData`. +### Signal + +Healthchecks uses [signal-cli](https://github.com/AsamK/signal-cli) to send Signal +notifications. It requires the `signal-cli` program to be installed and available on +the local machine. + +To send notifications, healthchecks executes "signal-cli send" calls. +It does not handle phone number registration and verification. You must do that +manually, before using the integration. + +To enable the Signal integration: + +* Download and install signal-cli in your preferred location + (for example, in `/srv/signal-cli-0.7.2/`). +* Register and verify phone number, or [link it](https://github.com/AsamK/signal-cli/wiki/Linking-other-devices-(Provisioning)) + to an existing registration. +* Test your signal-cli configuration by sending a message manually from command line. +* Put the sender phone number in the `SIGNAL_CLI_USERNAME` environment variable. + Example: `SIGNAL_CLI_USERNAME=+123456789`. +* If `signal-cli` is not in the system path, specify its path in `SIGNAL_CLI_CMD`. + Example: `SIGNAL_CLI_CMD=/srv/signal-cli-0.7.2/bin/signal-cli` + +It is possible to use a separate system user for running signal-cli: + +* Create a separate system user, (for example, "signal-user"). +* Configure signal-cli while logged in as signal-user. +* Change `SIGNAL_CLI_CMD` to run signal-cli through sudo: + `sudo -u signal-user /srv/signal-cli-0.7.2/bin/signal-cli`. +* Configure sudo to not require password. For example, if healthchecks + runs under the www-data system user, the sudoers rule would be: + `www-data ALL=(signal-user) NOPASSWD: /srv/signal-cli-0.7.2/bin/signal-cli`. + + ### Telegram * Create a Telegram bot by talking to the diff --git a/hc/api/models.py b/hc/api/models.py index 4cab92e9..6c1f202a 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -52,6 +52,7 @@ CHANNEL_KINDS = ( ("spike", "Spike"), ("call", "Phone Call"), ("linenotify", "LINE Notify"), + ("signal", "Signal"), ) PO_PRIORITIES = {-2: "lowest", -1: "low", 0: "normal", 1: "high", 2: "emergency"} @@ -471,6 +472,8 @@ class Channel(models.Model): return transports.Call(self) elif self.kind == "linenotify": return transports.LineNotify(self) + elif self.kind == "signal": + return transports.Signal(self) else: raise NotImplementedError("Unknown channel kind: %s" % self.kind) @@ -649,7 +652,7 @@ class Channel(models.Model): @property def phone_number(self): - assert self.kind in ("call", "sms", "whatsapp") + assert self.kind in ("call", "sms", "whatsapp", "signal") if self.value.startswith("{"): doc = json.loads(self.value) return doc["value"] @@ -714,6 +717,18 @@ class Channel(models.Model): doc = json.loads(self.value) return doc["down"] + @property + def signal_notify_up(self): + assert self.kind == "signal" + doc = json.loads(self.value) + return doc["up"] + + @property + def signal_notify_down(self): + assert self.kind == "signal" + doc = json.loads(self.value) + return doc["down"] + @property def opsgenie_key(self): assert self.kind == "opsgenie" diff --git a/hc/api/tests/test_notify_email.py b/hc/api/tests/test_notify_email.py index cbcc4408..ae989868 100644 --- a/hc/api/tests/test_notify_email.py +++ b/hc/api/tests/test_notify_email.py @@ -9,7 +9,7 @@ from hc.api.models import Channel, Check, Notification, Ping from hc.test import BaseTestCase -class NotifyTestCase(BaseTestCase): +class NotifyEmailTestCase(BaseTestCase): def setUp(self): super().setUp() diff --git a/hc/api/tests/test_notify_signal.py b/hc/api/tests/test_notify_signal.py new file mode 100644 index 00000000..46f25f58 --- /dev/null +++ b/hc/api/tests/test_notify_signal.py @@ -0,0 +1,82 @@ +# coding: utf-8 + +from datetime import timedelta as td +import json +from unittest.mock import patch + +from django.utils.timezone import now +from django.test.utils import override_settings +from hc.api.models import Channel, Check, Notification +from hc.test import BaseTestCase + + +@override_settings(SIGNAL_CLI_USERNAME="+987654321") +class NotifySignalTestCase(BaseTestCase): + def setUp(self): + super().setUp() + + self.check = Check(project=self.project) + self.check.name = "Daily Backup" + self.check.status = "down" + self.check.last_ping = now() - td(minutes=61) + self.check.save() + + payload = {"value": "+123456789", "up": True, "down": True} + self.channel = Channel(project=self.project) + self.channel.kind = "signal" + self.channel.value = json.dumps(payload) + self.channel.save() + self.channel.checks.add(self.check) + + @patch("hc.api.transports.subprocess.run") + def test_it_works(self, mock_run): + mock_run.return_value.returncode = 0 + + self.channel.notify(self.check) + + n = Notification.objects.get() + self.assertEqual(n.error, "") + + self.assertTrue(mock_run.called) + args, kwargs = mock_run.call_args + cmd = " ".join(args[0]) + + self.assertIn("-u +987654321", cmd) + self.assertIn("send +123456789", cmd) + + @patch("hc.api.transports.subprocess.run") + def test_it_obeys_down_flag(self, mock_run): + payload = {"value": "+123456789", "up": True, "down": False} + self.channel.value = json.dumps(payload) + self.channel.save() + + self.channel.notify(self.check) + + # This channel should not notify on "down" events: + self.assertEqual(Notification.objects.count(), 0) + self.assertFalse(mock_run.called) + + @patch("hc.api.transports.subprocess.run") + def test_it_requires_signal_cli_username(self, mock_run): + + with override_settings(SIGNAL_CLI_USERNAME=None): + self.channel.notify(self.check) + + n = Notification.objects.get() + self.assertEqual(n.error, "Signal notifications are not enabled") + + self.assertFalse(mock_run.called) + + @patch("hc.api.transports.subprocess.run") + def test_it_does_not_escape_special_characters(self, mock_run): + self.check.name = "Foo & Bar" + self.check.save() + + mock_run.return_value.returncode = 0 + self.channel.notify(self.check) + + self.assertTrue(mock_run.called) + args, kwargs = mock_run.call_args + cmd = " ".join(args[0]) + + self.assertIn("Foo & Bar", cmd) diff --git a/hc/api/transports.py b/hc/api/transports.py index ca5ca3ac..734c7ace 100644 --- a/hc/api/transports.py +++ b/hc/api/transports.py @@ -6,6 +6,7 @@ from django.utils import timezone from django.utils.html import escape import json import requests +import subprocess from urllib.parse import quote, urlencode from hc.accounts.models import Profile @@ -659,3 +660,27 @@ class LineNotify(HttpTransport): } payload = {"message": tmpl("linenotify_message.html", check=check)} return self.post(self.URL, headers=headers, params=payload) + + +class Signal(Transport): + def is_noop(self, check): + if check.status == "down": + return not self.channel.signal_notify_down + else: + return not self.channel.signal_notify_up + + def notify(self, check): + if not settings.SIGNAL_CLI_USERNAME: + return "Signal notifications are not enabled" + + text = tmpl("signal_message.html", check=check, site_name=settings.SITE_NAME) + + args = settings.SIGNAL_CLI_CMD.split() + args.extend(["-u", settings.SIGNAL_CLI_USERNAME]) + args.extend(["send", self.channel.phone_number]) + args.extend(["-m", text]) + + result = subprocess.run(args, timeout=10) + + if result.returncode != 0: + return "signal-cli returned exit code %d" % result.returncode diff --git a/hc/front/tests/test_add_signal.py b/hc/front/tests/test_add_signal.py new file mode 100644 index 00000000..de6e8a0a --- /dev/null +++ b/hc/front/tests/test_add_signal.py @@ -0,0 +1,60 @@ +from django.test.utils import override_settings +from hc.api.models import Channel +from hc.test import BaseTestCase + + +@override_settings(SIGNAL_CLI_USERNAME="+123456789") +class AddSignalTestCase(BaseTestCase): + def setUp(self): + super().setUp() + self.url = "/projects/%s/add_signal/" % self.project.code + + 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 Signal message") + + 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, self.channels_url) + + c = Channel.objects.get() + self.assertEqual(c.kind, "signal") + self.assertEqual(c.phone_number, "+1234567890") + self.assertEqual(c.name, "My Phone") + self.assertTrue(c.signal_notify_down) + self.assertTrue(c.signal_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, self.channels_url) + + c = Channel.objects.get() + self.assertFalse(c.signal_notify_down) + self.assertFalse(c.signal_notify_up) + + @override_settings(SIGNAL_CLI_USERNAME=None) + def test_it_handles_unset_username(self): + self.client.login(username="alice@example.org", password="password") + r = self.client.get(self.url) + self.assertEqual(r.status_code, 404) + + def test_it_requires_rw_access(self): + self.bobs_membership.rw = False + self.bobs_membership.save() + + self.client.login(username="bob@example.org", password="password") + r = self.client.get(self.url) + self.assertEqual(r.status_code, 403) diff --git a/hc/front/urls.py b/hc/front/urls.py index 7b585b26..0f20f490 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -77,6 +77,7 @@ project_urls = [ path("add_zulip/", views.add_zulip, name="hc-add-zulip"), path("add_spike/", views.add_spike, name="hc-add-spike"), path("add_linenotify/", views.add_linenotify, name="hc-add-linenotify"), + path("add_signal/", views.add_signal, name="hc-add-signal"), path("badges/", views.badges, name="hc-badges"), path("checks/", views.my_checks, name="hc-checks"), path("checks/add/", views.add_check, name="hc-add-check"), diff --git a/hc/front/views.py b/hc/front/views.py index 7d045a8a..9be6f049 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -299,6 +299,7 @@ def index(request): "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None, "enable_pushover": settings.PUSHOVER_API_TOKEN is not None, "enable_shell": settings.SHELL_ENABLED is True, + "enable_signal": settings.SIGNAL_CLI_USERNAME is not None, "enable_slack_btn": settings.SLACK_CLIENT_ID is not None, "enable_sms": settings.TWILIO_AUTH is not None, "enable_telegram": settings.TELEGRAM_TOKEN is not None, @@ -762,6 +763,7 @@ def channels(request, code): "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None, "enable_pushover": settings.PUSHOVER_API_TOKEN is not None, "enable_shell": settings.SHELL_ENABLED is True, + "enable_signal": settings.SIGNAL_CLI_USERNAME is not None, "enable_slack_btn": settings.SLACK_CLIENT_ID is not None, "enable_sms": settings.TWILIO_AUTH is not None, "enable_telegram": settings.TELEGRAM_TOKEN is not None, @@ -1632,6 +1634,38 @@ def add_whatsapp(request, code): return render(request, "integrations/add_whatsapp.html", ctx) +@require_setting("SIGNAL_CLI_USERNAME") +@login_required +def add_signal(request, code): + project = _get_rw_project_for_user(request, code) + if request.method == "POST": + form = forms.AddSmsForm(request.POST) + if form.is_valid(): + channel = Channel(project=project, kind="signal") + 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", project.code) + else: + form = forms.AddSmsForm() + + ctx = { + "page": "channels", + "project": project, + "form": form, + "profile": project.owner_profile, + } + return render(request, "integrations/add_signal.html", ctx) + + @require_setting("TRELLO_APP_KEY") @login_required def add_trello(request, code): diff --git a/hc/settings.py b/hc/settings.py index ab40ea9d..daa9664a 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -230,6 +230,10 @@ SHELL_ENABLED = envbool("SHELL_ENABLED", "False") LINENOTIFY_CLIENT_ID = os.getenv("LINENOTIFY_CLIENT_ID") LINENOTIFY_CLIENT_SECRET = os.getenv("LINENOTIFY_CLIENT_SECRET") +# Signal +SIGNAL_CLI_USERNAME = os.getenv("SIGNAL_CLI_USERNAME") +SIGNAL_CLI_CMD = os.getenv("SIGNAL_CLI_CMD", "signal-cli") + if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")): from .local_settings import * else: diff --git a/static/css/icomoon.css b/static/css/icomoon.css index 0b18c9fc..5e1e896b 100644 --- a/static/css/icomoon.css +++ b/static/css/icomoon.css @@ -1,10 +1,10 @@ @font-face { font-family: 'icomoon'; - src: url('../fonts/icomoon.eot?qka09c'); - src: url('../fonts/icomoon.eot?qka09c#iefix') format('embedded-opentype'), - url('../fonts/icomoon.ttf?qka09c') format('truetype'), - url('../fonts/icomoon.woff?qka09c') format('woff'), - url('../fonts/icomoon.svg?qka09c#icomoon') format('svg'); + src: url('../fonts/icomoon.eot?y9u69e'); + src: url('../fonts/icomoon.eot?y9u69e#iefix') format('embedded-opentype'), + url('../fonts/icomoon.ttf?y9u69e') format('truetype'), + url('../fonts/icomoon.woff?y9u69e') format('woff'), + url('../fonts/icomoon.svg?y9u69e#icomoon') format('svg'); font-weight: normal; font-style: normal; } @@ -24,6 +24,10 @@ -moz-osx-font-smoothing: grayscale; } +.icon-signal:before { + content: "\e91c"; + color: #2592e9; +} .icon-linenotify:before { content: "\e91b"; color: #00c300; diff --git a/static/fonts/icomoon.eot b/static/fonts/icomoon.eot index 23dc62333c33566e843200391711e1b6001dbee0..a54397055ccfc8bd3f22bbdfc9effc24dbf1276d 100644 GIT binary patch delta 1093 zcmYjQU1(cn7=GXHJ2^i|PSTwJpPVEn*n`TNX{gn}jH_61 z$S^}egdtesg%v>&H@(=!pk9fNUMcLx3(>1ycrig_l-+n`+xecZ1L2(Wo#*F$zK0LK z?_Rs#7w@+KPPNv=mcZ{^y0>=X?d%Ky{6O!^Oa1Q3JKwDRR2cky@#Ciea1$Ur-&^j! z;@`OXJHVtvu2zo-IVH3SpCMfB^;fUltjQyUR|(&K{qj=xw^x7u3}C8C_(H#XWd%;+ z1H#t`m)_|1m)ASWzl6V}Jw8~u{MIUPQlRx@tOMu6B|L%o;n(;g<~JC--WV8w4>$Qk zz6s-S96GQF7oi7l!n^Pxtid{b3*WS0-!6h_dXcft9i-k)Os0#3~Lb$tHIN=~E*>Pl5y~_JsU^ zwEw5tA;!LVDC7r8lD+S5AWAhUj&E92W-^Knm&b7ovy?@Q)wCl!gO#!U__%E?96U}A z@gTHzh_G~A3ms&k3!{J4_}G*d66d&U#0tyWB@G~?>XIdWtSqjjF(IuqDp z`m`VmEWyMt%u!Prkx|YmR()U!yW8o4Zt5kK&l+_nElE`(!a^M!)_9wG`7;wedt`MHIK*}3H)IK!xq zkI1+uBywpN1*T+$y+JiN>dV11Mf@~vh+ZNs(ZMX5m8_j98^+F-Y-McKlswMN%7~?w zG%j*|ubA|kcGj8B<1f~PnH{SIi!kJr_2z&_(VKv#y!^e zLp$e8<@bE^&ZQru@l(v LB-1yt%^Ub1nPIWn delta 252 zcmaEpyCt1%i!lR(fbm2&GnS)QblfL8l=E9MFfi-^;)LYf#Dc126OYN&UstKqVPIf9 zz`&rVk&&91BBd|j!N8#Z0;tL?11P}3&0@{KV4wixt7PPsR4h{G;0N+ufc!N%`N@fU zb{(GuD$q2yNCg-Ny(3JMThZ diff --git a/static/fonts/icomoon.svg b/static/fonts/icomoon.svg index 6c7ea3a0..f4270f68 100644 --- a/static/fonts/icomoon.svg +++ b/static/fonts/icomoon.svg @@ -46,4 +46,5 @@ + \ No newline at end of file diff --git a/static/fonts/icomoon.ttf b/static/fonts/icomoon.ttf index c1742fa582842e1bf985d974d249e5e5c6ae3488..06389143dfc987665d2541ae2e774004624e2169 100644 GIT binary patch delta 1112 zcmYjQOKcle6uo!ecs}-w9nb&cv5oE4Bv_8e`Aic`E0m}Zqm)9rD3u6UC8?#xG^i0O zsKOCQ2&qD9$%ddxP^9vax~P;*g#cv->V^e^RTeB@!y@X26>-X)G?jRonLFp)bI&_^ z(!0L=^GY8O08+3H3>q&kj5afxFip(&^uB(+*I9k{>$krJz#QS57dn?$sSXjoM^(D8 z^3L+#i`SnbybTbZ?JjlR@NZxJ9bn9%{$!U3DaE%4pC(-C_SUX^Rh4LdY>n{Ul}qQR zI=?Od^clc-g@|*#&XrYo79S8^CtQ59(_7kX%l{Jof;RbZ_0r`v3QBA8$s6~c!`vtM zD(1Ep@mjra06yGi57{=1!U<@@0xUuo-h%hwBe(&Z@C|$i_u*IAfxloE_D~|jl9)zO zFbzA8wq=KQn8VP_<5+Vt3ZuA2$1$3m7_Zi`dbB)NJx-KmM|CWh1d;SKMj-BDpPXz` zmyteI0`w%PA;y}J9g_C{x^{@ME*=Wmp_6DI=U|5=`fKZb3Q4OFWn0gwM@kQy!8~%F0%)Z}Nj(C9j)$QDrkm%}INsrm^>f zAfS(u;A+Vv=W$8d-EzrJOpBVRijI3u&!rsNA(hj0_EHcm&Cbk+BO`P9y!}2R@*?8% zq7d`5^Yb&aOF?j&qdYz$$2>lfRb1pbIm7SwE5Vd61-nLbb;fi1zIo)8AuC(8w42_sVyWee=m)I=#*qg1Pa%&&O4J1< z(Uv~8LSN=Z=G9G&?ooXEY5jTeNA_B#^$(=@1H4nnMe$EuH>fsuiMft#U$ftkU;KUm+0Ux>{TD6$8L6OwZi3#yittz=+elmYVRq$d^^ z0BHdre+Q7}NYANEyRK5F1LPlIV9?XZNKH(U(iiYxV9s&b+&u{aUftv-)zn%OP sWHbYao@`@m&Zs@P#CXkQ6_W@7u-CO2-!TY*RWeOp$SAydvB^?K0N;8<3;+NC diff --git a/static/fonts/icomoon.woff b/static/fonts/icomoon.woff index c13e08bfc7812c91cd17432e4a922f5cc5fd2405..6b419e8fa185224b69a4665687e1596cf3214197 100644 GIT binary patch delta 1136 zcmYjRO=uit82;Y*X6I*jW_EY}e|C1)-EEh|Ws})WGHqN_N)2itq7OX5a>4IHb zY0xUg6$KHB(2yRC6a}IF(SwkiV98A=f(Ox44j%L(61;daoBDp+4LHm*AMf)%-}62* z@Xg-(ou2vTxy3~S1h1JpBsE;mY@Kepn>C|9d#TsCMhJ9~zRU3Tmv4N1xpN&kj>lRW zA#?f4TkFUvghZ)Gub15 zkbQCh_3BgSA+ABQSn%27Nf;o$sIyH_kG z<+A4$l9(MRcQcsHKc(~A`g|g*c))XVnm_DU!jpj%K3#y16;tpNiijU( z#VV&Atz?=9dy=g=swMiAv&tD;E$URD#$h2DG@P_Mor9n3Nl(Ed`)5Xa7(XA zvy{ga?lXW9D{hGc@YxsfF&Xu_UJyB1cPe)nSjS$>*x9PB-}O%vORYeJAZ!(YMH)Oj z3p@xZ(GZkGTl&b30+|=7U$=BTXTyI3A-})+fWDk={XRx4W7-22#Nduy;y%$-l82k;*+|F_Wq delta 318 zcmbP|dnZk#+~3WOfsp|SjAIzM!L-2SkBlOdm{=z22-jOC=Oz{~Ffe8SW#)iz)v~gc z>50WaF(wuup96{o(sL@)fMPrh40;+Md|jnZCnGg6g@M7q1gORggr)QaJTicSK(QPk zUj>9YxLK?-a!V?JViSP;H6Xl5or6CoKN+ab&d>%Z5COt_b{(IUn^*y~*l-GvuK>n& zj34q6b5nt0TY!#i1L2uooWBe5i%WnWvYGhDpHX_U4`VE&^yXEJ-FnJkk1{a0>s&b+ z&u{aUfg9up28P?oPd~uu$sNY#jM|eA7_XUJU=kq!4jXO8cML*cl}x~}5#IdXWGN#6 DJa%<#I(qLtb?`iO@jQ6%Ja+RvcJMxS z^gwv;Ja_Lrb?`iP@H}?#J$UaucJDoP@;i3#Kzi>xbniWQ@;!C$J#_CrcJMuS@;!R* zKXvjwdG9}X@<4g-K6mpyck?`V@jP_%Ja+FsbniTN@FoF8D*ylhEOb&%Qvl!JV84Js zAQ0~`zi*I#uU|lr&(E)*P@ixxaG$WC&re{VP@hnbk8gi~fCEgGQvd)5NJ&INRCwC$ zT-SE1IuJD=kVG%WcBK) zt3RUGAJ6%wh)5W)ht|(Is^ECnPwv^PbGjA5vz$sgz}KJb0+pw4K?vE`*Y)F_?wj8}gm|UiJ2Iz4u*H@DOdGgzr zJh5c2V+Hxgy&BMu*NU|E<9H|x49$W43Ms1}3STjDV0h-DVZ3qIQBA8LPbSc5iG>3+akYxXGM=wn(2$^7NfILA znPKm55*ln*s|ok1?%b3EQxT27BAPxPIuXA}`}6}tL#tJVGC$VZmqq3fhdG6{(R6r7 zAv=AgxKO9-qv`N8oQ$T(<9&b4-wsWO$M5BIYBHDZ`ZlHF^8;Fvh>?xZO!AkhMf3t? zwV5C>LnH$USAzS_c@qKd7zgTLfram7)u!SJS(Iu6{|xnr?QP*O`V@-A+kpa+3$bk| zZFcb>Dyx$wZd>g(t=e2#1h!f|84R(+-X2c7T%KAz&rmsWr9oU+k3b5uV#>if(6Hn} zsUAT&&syLxHlwN+OWUMNr*gPP8ud`aa-Ef{pH#bx_ z96E;PzN*ZSXY%MumpJnAPLPKg-kI<^An(E-VII722rs}P8sM_Sp(Vgu+xp>HfVA&i zBLw(oy#n0to@>OEVKLL##Z_V~!62N$ z2Y}I7`jzp_FbDij^uvL<#5FlRmQi>#p1Kn+jF5be?U=IjffF`UhV@5mihoJT0Ee&c zaDVoe(y%v7Qr`PW6658jYs8}LB$k4WpJqq6he{AyTpbfBPoyYS!a}ZRVyzO8hw(Bu z5xRjjuGLN~bGJd=(QjMu++i;@I|>zfJw6bwRV2p-qC1Q}_K&Z6M>vL#*I-xU1B@R? zuX0*f2a}7IFfV#-9Rnf@N3$(qkkGZJRXO6AfN;2usdnYa)o!8pKue@>hm|9#M~Cle zTS6T8+@5+YZML3{Hq0>fq?$^Wk3uhuB4I8%Ti@O>v);i%$(&AYSkHl|*xHSUjRng=5>`)d@>&F(@S-@ojFaOMMv>K4~@eFPi%*&hGQhwP>#nMguO0( z-QP zONUY~x{hivpVJ#L@t{k+R85sAIudewU?_roe4(_vUc~IwS5Sezl4|33`DjoBs1}uRgWeIz%khI^?4P(Uf0J;NAGcv)~J+L zI_t*c!^s$a94f~XIFoP5$2_NUp1xUI{;-xM0*flk=d_~K|Jm=r%5#M}E`qz-h z;|lh~0MXdthdh^u?=On-;sP)Ol~Njw2Z)qAEGr0t*KbLJ=9Vigk&Z^*GWy$nFQ)zr z#Nlh^4{i`&gd`4yi*E(-p&L7mUr2@BqO-%PtE7;7(~Tf)Qe=;XPIz#Ns_BciDEh%g zTV5C*muM9aH}F1n`&rH6M5P$D?BW!??aT_n*!MiV$`yA10a9zHLLl{%X8-^I07*qo IM6N<$g7aA@&;S4c literal 0 HcmV?d00001 diff --git a/templates/front/channels.html b/templates/front/channels.html index c4ba7d06..8535680e 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -91,6 +91,14 @@ {% elif ch.zulip_type == "private" %} user {{ ch.zulip_to}} {% endif %} + {% elif ch.kind == "signal" %} + Signal to {{ ch.phone_number }} + {% if ch.signal_notify_down and not ch.signal_notify_up %} + (down only) + {% endif %} + {% if ch.signal_notify_up and not ch.signal_notify_down %} + (up only) + {% endif %} {% else %} {{ ch.get_kind_display }} {% endif %} @@ -355,6 +363,17 @@ {% endif %} + {% if enable_signal %} +
  • + Signal icon + +

    Signal

    +

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

    + Add Integration +
  • + {% endif %} + {% if enable_sms %}
  • {% endif %} + {% if enable_signal %} +
    +
    + +

    + {% trans "Signal" %}
    + {% trans "Chat" %} +

    +
    +
    + {% endif %} + {% if enable_sms %}
    diff --git a/templates/integrations/add_signal.html b/templates/integrations/add_signal.html new file mode 100644 index 00000000..461759d8 --- /dev/null +++ b/templates/integrations/add_signal.html @@ -0,0 +1,98 @@ +{% extends "base.html" %} +{% load humanize static hc_extras %} + +{% block title %}Add Signal Integration - {{ site_name }}{% endblock %} + + +{% block content %} +
    +
    +

    Signal

    + +

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

    + +

    Integration Settings

    + +
    + {% csrf_token %} +
    + +
    + + + {% if form.label.errors %} +
    + {{ form.label.errors|join:"" }} +
    + {% else %} + + Optional. If you add multiple phone numbers, + the labels will help you tell them apart. + + {% endif %} +
    +
    + +
    + +
    + + + {% if form.value.errors %} +
    + {{ form.value.errors|join:"" }} +
    + {% else %} + + Make sure the phone number starts with "+" and has the + country code. + + {% endif %} +
    +
    + +
    + +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +{% endblock %} diff --git a/templates/integrations/signal_message.html b/templates/integrations/signal_message.html new file mode 100644 index 00000000..253c8dac --- /dev/null +++ b/templates/integrations/signal_message.html @@ -0,0 +1,7 @@ +{% load humanize %}{% spaceless %} +{% if check.status == "down" %} + The check “{{ check.name_then_code|safe }}” is DOWN. Last ping was {{ check.last_ping|naturaltime }}. +{% else %} + The check “{{ check.name_then_code|safe }}” is now UP. +{% endif %} +{% endspaceless %}