diff --git a/README.md b/README.md index 9f5d0ae6..541eecf9 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,8 @@ Configurations settings loaded from environment variables: | MATRIX_ACCESS_TOKEN | `None` | APPRISE_ENABLED | `"False"` | SHELL_ENABLED | `"False"` +| LINENOTIFY_CLIENT_ID | `None` +| LINENOTIFY_CLIENT_SECRET | `None` Some useful settings keys to override are: diff --git a/hc/front/tests/test_add_linenotify.py b/hc/front/tests/test_add_linenotify.py index 08b540e8..c2b5a986 100644 --- a/hc/front/tests/test_add_linenotify.py +++ b/hc/front/tests/test_add_linenotify.py @@ -1,10 +1,9 @@ -from hc.api.models import Channel +from django.test.utils import override_settings from hc.test import BaseTestCase +@override_settings(LINENOTIFY_CLIENT_ID="t1", LINENOTIFY_CLIENT_SECRET="s1") class AddLineNotifyTestCase(BaseTestCase): - url = "/integrations/add_linenotify/" - def setUp(self): super(AddLineNotifyTestCase, self).setUp() self.url = "/projects/%s/add_linenotify/" % self.project.code @@ -12,32 +11,17 @@ class AddLineNotifyTestCase(BaseTestCase): def test_instructions_work(self): self.client.login(username="alice@example.org", password="password") r = self.client.get(self.url) - self.assertContains(r, "LineNotify") - - def test_it_works(self): - form = {"token": "helloworld"} - - self.client.login(username="alice@example.org", password="password") - r = self.client.post(self.url, form) - self.assertRedirects(r, self.channels_url) + self.assertContains(r, "notify-bot.line.me/oauth/authorize", status_code=200) + self.assertContains(r, "Connect LINE Notify") - c = Channel.objects.get() - self.assertEqual(c.kind, "linenotify") - self.assertEqual(c.value, "helloworld") - self.assertEqual(c.project, self.project) - - def test_it_handles_json_linenotify_value(self): - c = Channel(kind="linenotify", value="foo123") - self.assertEqual(c.linenotify_token, "foo123") - - def test_it_save_token(self): - form = {"token": "foo123"} + # There should now be a key in session + self.assertTrue("add_linenotify" in self.client.session) + @override_settings(LINENOTIFY_CLIENT_ID=None) + def test_it_requires_client_id(self): self.client.login(username="alice@example.org", password="password") - self.client.post(self.url, form) - - c = Channel.objects.get() - self.assertEqual(c.value, "foo123") + r = self.client.get(self.url) + self.assertEqual(r.status_code, 404) def test_it_requires_rw_access(self): self.bobs_membership.rw = False diff --git a/hc/front/tests/test_add_linenotify_complete.py b/hc/front/tests/test_add_linenotify_complete.py new file mode 100644 index 00000000..cfa94a97 --- /dev/null +++ b/hc/front/tests/test_add_linenotify_complete.py @@ -0,0 +1,81 @@ +from unittest.mock import patch + +from django.test.utils import override_settings +from hc.api.models import Channel +from hc.test import BaseTestCase + + +@override_settings(LINENOTIFY_CLIENT_ID="t1", LINENOTIFY_CLIENT_SECRET="s1") +class AddLineNotifyCompleteTestCase(BaseTestCase): + url = "/integrations/add_linenotify/" + + @patch("hc.front.views.requests") + def test_it_handles_oauth_response(self, mock_requests): + session = self.client.session + session["add_linenotify"] = ("foo", str(self.project.code)) + session.save() + + mock_requests.post.return_value.json.return_value = { + "status": 200, + "access_token": "test-token", + } + + mock_requests.get.return_value.json.return_value = {"target": "Alice"} + + url = self.url + "?code=12345678&state=foo" + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(url, follow=True) + self.assertRedirects(r, self.channels_url) + self.assertContains(r, "The LINE Notify integration has been added!") + + ch = Channel.objects.get() + self.assertEqual(ch.value, "test-token") + self.assertEqual(ch.name, "Alice") + self.assertEqual(ch.project, self.project) + + # Session should now be clean + self.assertFalse("add_linenotify" in self.client.session) + + def test_it_avoids_csrf(self): + session = self.client.session + session["add_linenotify"] = ("foo", str(self.project.code)) + session.save() + + url = self.url + "?code=12345678&state=bar" + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(url) + self.assertEqual(r.status_code, 403) + + def test_it_handles_denial(self): + session = self.client.session + session["add_linenotify"] = ("foo", str(self.project.code)) + session.save() + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(self.url + "?error=access_denied&state=foo", follow=True) + self.assertRedirects(r, self.channels_url) + self.assertContains(r, "LINE Notify setup was cancelled") + + self.assertEqual(Channel.objects.count(), 0) + + # Session should now be clean + self.assertFalse("add_linenotify" in self.client.session) + + @override_settings(LINENOTIFY_CLIENT_ID=None) + def test_it_requires_client_id(self): + url = self.url + "?code=12345678&state=bar" + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(url) + self.assertEqual(r.status_code, 404) + + def test_it_requires_rw_access(self): + self.bobs_membership.rw = False + self.bobs_membership.save() + + url = self.url + "?code=12345678&state=bar" + self.client.login(username="bob@example.org", password="password") + r = self.client.get(url) + self.assertEqual(r.status_code, 403) diff --git a/hc/front/tests/test_add_pushbullet_complete.py b/hc/front/tests/test_add_pushbullet_complete.py index ed834487..71e34982 100644 --- a/hc/front/tests/test_add_pushbullet_complete.py +++ b/hc/front/tests/test_add_pushbullet_complete.py @@ -37,7 +37,7 @@ class AddPushbulletTestCase(BaseTestCase): def test_it_avoids_csrf(self): session = self.client.session - session["pushbullet"] = ("foo", str(self.project.code)) + session["add_pushbullet"] = ("foo", str(self.project.code)) session.save() url = self.url + "?code=12345678&state=bar&project=%s" % self.project.code diff --git a/hc/front/urls.py b/hc/front/urls.py index eee31f7a..0d047086 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -30,6 +30,7 @@ channel_urls = [ name="hc-add-pushbullet-complete", ), path("add_discord/", views.add_discord_complete, name="hc-add-discord-complete"), + path("add_linenotify/", views.add_linenotify_complete), path("add_pushover/", views.pushover_help, name="hc-pushover-help"), path("telegram/", views.telegram_help, name="hc-telegram-help"), path("telegram/bot/", views.telegram_bot, name="hc-telegram-webhook"), diff --git a/hc/front/views.py b/hc/front/views.py index a5dc81fd..2dd82ad1 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -290,7 +290,9 @@ def index(request): "check": check, "ping_url": check.url(), "enable_apprise": settings.APPRISE_ENABLED is True, + "enable_call": settings.TWILIO_AUTH is not None, "enable_discord": settings.DISCORD_CLIENT_ID is not None, + "enable_linenotify": settings.LINENOTIFY_CLIENT_ID is not None, "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None, "enable_pdc": settings.PD_VENDOR_KEY is not None, "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None, @@ -298,7 +300,6 @@ def index(request): "enable_shell": settings.SHELL_ENABLED is True, "enable_slack_btn": settings.SLACK_CLIENT_ID is not None, "enable_sms": settings.TWILIO_AUTH is not None, - "enable_call": settings.TWILIO_AUTH is not None, "enable_telegram": settings.TELEGRAM_TOKEN is not None, "enable_trello": settings.TRELLO_APP_KEY is not None, "enable_whatsapp": settings.TWILIO_USE_WHATSAPP, @@ -738,7 +739,9 @@ def channels(request, code): "profile": project.owner_profile, "channels": channels, "enable_apprise": settings.APPRISE_ENABLED is True, + "enable_call": settings.TWILIO_AUTH is not None, "enable_discord": settings.DISCORD_CLIENT_ID is not None, + "enable_linenotify": settings.LINENOTIFY_CLIENT_ID is not None, "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None, "enable_pdc": settings.PD_VENDOR_KEY is not None, "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None, @@ -746,7 +749,6 @@ def channels(request, code): "enable_shell": settings.SHELL_ENABLED is True, "enable_slack_btn": settings.SLACK_CLIENT_ID is not None, "enable_sms": settings.TWILIO_AUTH is not None, - "enable_call": settings.TWILIO_AUTH is not None, "enable_telegram": settings.TELEGRAM_TOKEN is not None, "enable_trello": settings.TRELLO_APP_KEY is not None, "enable_whatsapp": settings.TWILIO_USE_WHATSAPP, @@ -1819,21 +1821,82 @@ def add_spike(request, code): return render(request, "integrations/add_spike.html", ctx) +@require_setting("LINENOTIFY_CLIENT_ID") @login_required def add_linenotify(request, code): project = _get_rw_project_for_user(request, code) + redirect_uri = settings.SITE_ROOT + reverse(add_linenotify_complete) - if request.method == "POST": - form = forms.AddLineNotifyForm(request.POST) - if form.is_valid(): - channel = Channel(project=project, kind="linenotify") - channel.value = form.cleaned_data["token"] - channel.save() + state = token_urlsafe() + authorize_url = " https://notify-bot.line.me/oauth/authorize?" + urlencode( + { + "client_id": settings.LINENOTIFY_CLIENT_ID, + "redirect_uri": redirect_uri, + "response_type": "code", + "state": state, + "scope": "notify", + } + ) - channel.assign_all_checks() - return redirect("hc-p-channels", project.code) - else: - form = forms.AddLineNotifyForm() + ctx = { + "page": "channels", + "project": project, + "authorize_url": authorize_url, + } - ctx = {"page": "channels", "project": project, "form": form} + request.session["add_linenotify"] = (state, str(project.code)) return render(request, "integrations/add_linenotify.html", ctx) + + +@require_setting("LINENOTIFY_CLIENT_ID") +@login_required +def add_linenotify_complete(request): + if "add_linenotify" not in request.session: + return HttpResponseForbidden() + + state, code = request.session.pop("add_linenotify") + if request.GET.get("state") != state: + return HttpResponseForbidden() + + project = _get_rw_project_for_user(request, code) + if request.GET.get("error") == "access_denied": + messages.warning(request, "LINE Notify setup was cancelled.") + return redirect("hc-p-channels", project.code) + + # Exchange code for access token + redirect_uri = settings.SITE_ROOT + reverse(add_linenotify_complete) + result = requests.post( + "https://notify-bot.line.me/oauth/token", + { + "grant_type": "authorization_code", + "code": request.GET.get("code"), + "redirect_uri": redirect_uri, + "client_id": settings.LINENOTIFY_CLIENT_ID, + "client_secret": settings.LINENOTIFY_CLIENT_SECRET, + }, + ) + + doc = result.json() + if doc.get("status") != 200: + messages.warning(request, "Something went wrong.") + return redirect("hc-p-channels", project.code) + + # Fetch notification target's name, will use it as channel name: + token = doc["access_token"] + result = requests.get( + "https://notify-api.line.me/api/status", + headers={"Authorization": "Bearer %s" % token}, + ) + doc = result.json() + + channel = Channel(kind="linenotify", project=project) + channel.name = doc.get("target") + channel.value = token + channel.save() + channel.assign_all_checks() + messages.success(request, "The LINE Notify integration has been added!") + + return redirect("hc-p-channels", project.code) + + +# Forks: add custom views after this line diff --git a/hc/settings.py b/hc/settings.py index a97ac395..1a4557d1 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -219,6 +219,9 @@ APPRISE_ENABLED = envbool("APPRISE_ENABLED", "False") # Local shell commands SHELL_ENABLED = envbool("SHELL_ENABLED", "False") +# LINE Notify +LINENOTIFY_CLIENT_ID = os.getenv("LINENOTIFY_CLIENT_ID") +LINENOTIFY_CLIENT_SECRET = os.getenv("LINENOTIFY_CLIENT_SECRET") if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")): from .local_settings import * diff --git a/static/img/integrations/setup_linenotify_1.png b/static/img/integrations/setup_linenotify_1.png deleted file mode 100644 index c004ee1f..00000000 Binary files a/static/img/integrations/setup_linenotify_1.png and /dev/null differ diff --git a/static/img/integrations/setup_linenotify_2.png b/static/img/integrations/setup_linenotify_2.png deleted file mode 100644 index f735cb44..00000000 Binary files a/static/img/integrations/setup_linenotify_2.png and /dev/null differ diff --git a/static/img/integrations/setup_linenotify_3.png b/static/img/integrations/setup_linenotify_3.png deleted file mode 100644 index 1286d895..00000000 Binary files a/static/img/integrations/setup_linenotify_3.png and /dev/null differ diff --git a/templates/front/channels.html b/templates/front/channels.html index 17f56dd0..c58a473d 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -230,6 +230,7 @@ {% endif %} + {% if enable_linenotify %}
Receive a notification on LINE when a check goes up or down.
Add IntegrationLINE Notify - allows you to send notifications directly to your LINE chats. - You can set up {% site_name %} to post status updates directly to an - appropriate LINE chat.
- - -- Log into your LINE account, - go to My Page in the upper right corner. - In the My Page, scroll to the bottom of the page, - and click on Generate token button. -
-- It will pop up the Generate token Form. - Fill out the details, and click the button. -
-- Copy the displayed Token and paste it - in the form below. - Save the integration, and it's done! -
-+ With the LINE Notify + integration, {{ site_name }} will send + a notification to your selected LINE chat when a check + goes up or down. +
+ + +