Browse Source

THe LINE Notify integration uses OAuth2 flow.

pull/419/head
Pēteris Caune 4 years ago
parent
commit
4f53325730
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
13 changed files with 200 additions and 136 deletions
  1. +2
    -0
      README.md
  2. +10
    -26
      hc/front/tests/test_add_linenotify.py
  3. +81
    -0
      hc/front/tests/test_add_linenotify_complete.py
  4. +1
    -1
      hc/front/tests/test_add_pushbullet_complete.py
  5. +1
    -0
      hc/front/urls.py
  6. +76
    -13
      hc/front/views.py
  7. +3
    -0
      hc/settings.py
  8. BIN
      static/img/integrations/setup_linenotify_1.png
  9. BIN
      static/img/integrations/setup_linenotify_2.png
  10. BIN
      static/img/integrations/setup_linenotify_3.png
  11. +2
    -0
      templates/front/channels.html
  12. +2
    -0
      templates/front/welcome.html
  13. +22
    -96
      templates/integrations/add_linenotify.html

+ 2
- 0
README.md View File

@ -136,6 +136,8 @@ Configurations settings loaded from environment variables:
| MATRIX_ACCESS_TOKEN | `None` | MATRIX_ACCESS_TOKEN | `None`
| APPRISE_ENABLED | `"False"` | APPRISE_ENABLED | `"False"`
| SHELL_ENABLED | `"False"` | SHELL_ENABLED | `"False"`
| LINENOTIFY_CLIENT_ID | `None`
| LINENOTIFY_CLIENT_SECRET | `None`
Some useful settings keys to override are: Some useful settings keys to override are:


+ 10
- 26
hc/front/tests/test_add_linenotify.py View File

@ -1,10 +1,9 @@
from hc.api.models import Channel
from django.test.utils import override_settings
from hc.test import BaseTestCase from hc.test import BaseTestCase
@override_settings(LINENOTIFY_CLIENT_ID="t1", LINENOTIFY_CLIENT_SECRET="s1")
class AddLineNotifyTestCase(BaseTestCase): class AddLineNotifyTestCase(BaseTestCase):
url = "/integrations/add_linenotify/"
def setUp(self): def setUp(self):
super(AddLineNotifyTestCase, self).setUp() super(AddLineNotifyTestCase, self).setUp()
self.url = "/projects/%s/add_linenotify/" % self.project.code self.url = "/projects/%s/add_linenotify/" % self.project.code
@ -12,32 +11,17 @@ class AddLineNotifyTestCase(BaseTestCase):
def test_instructions_work(self): def test_instructions_work(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertContains(r, "LineNotify")
def test_it_works(self):
form = {"token": "helloworld"}
self.client.login(username="[email protected]", 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="[email protected]", password="password") self.client.login(username="[email protected]", 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): def test_it_requires_rw_access(self):
self.bobs_membership.rw = False self.bobs_membership.rw = False


+ 81
- 0
hc/front/tests/test_add_linenotify_complete.py View File

@ -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="[email protected]", 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="[email protected]", 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="[email protected]", 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="[email protected]", 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="[email protected]", password="password")
r = self.client.get(url)
self.assertEqual(r.status_code, 403)

+ 1
- 1
hc/front/tests/test_add_pushbullet_complete.py View File

@ -37,7 +37,7 @@ class AddPushbulletTestCase(BaseTestCase):
def test_it_avoids_csrf(self): def test_it_avoids_csrf(self):
session = self.client.session session = self.client.session
session["pushbullet"] = ("foo", str(self.project.code))
session["add_pushbullet"] = ("foo", str(self.project.code))
session.save() session.save()
url = self.url + "?code=12345678&state=bar&project=%s" % self.project.code url = self.url + "?code=12345678&state=bar&project=%s" % self.project.code


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

@ -30,6 +30,7 @@ channel_urls = [
name="hc-add-pushbullet-complete", name="hc-add-pushbullet-complete",
), ),
path("add_discord/", views.add_discord_complete, name="hc-add-discord-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("add_pushover/", views.pushover_help, name="hc-pushover-help"),
path("telegram/", views.telegram_help, name="hc-telegram-help"), path("telegram/", views.telegram_help, name="hc-telegram-help"),
path("telegram/bot/", views.telegram_bot, name="hc-telegram-webhook"), path("telegram/bot/", views.telegram_bot, name="hc-telegram-webhook"),


+ 76
- 13
hc/front/views.py View File

@ -290,7 +290,9 @@ def index(request):
"check": check, "check": check,
"ping_url": check.url(), "ping_url": check.url(),
"enable_apprise": settings.APPRISE_ENABLED is True, "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_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_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
"enable_pdc": settings.PD_VENDOR_KEY is not None, "enable_pdc": settings.PD_VENDOR_KEY is not None,
"enable_pushbullet": settings.PUSHBULLET_CLIENT_ID 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_shell": settings.SHELL_ENABLED is True,
"enable_slack_btn": settings.SLACK_CLIENT_ID is not None, "enable_slack_btn": settings.SLACK_CLIENT_ID is not None,
"enable_sms": settings.TWILIO_AUTH 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_telegram": settings.TELEGRAM_TOKEN is not None,
"enable_trello": settings.TRELLO_APP_KEY is not None, "enable_trello": settings.TRELLO_APP_KEY is not None,
"enable_whatsapp": settings.TWILIO_USE_WHATSAPP, "enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
@ -738,7 +739,9 @@ def channels(request, code):
"profile": project.owner_profile, "profile": project.owner_profile,
"channels": channels, "channels": channels,
"enable_apprise": settings.APPRISE_ENABLED is True, "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_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_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
"enable_pdc": settings.PD_VENDOR_KEY is not None, "enable_pdc": settings.PD_VENDOR_KEY is not None,
"enable_pushbullet": settings.PUSHBULLET_CLIENT_ID 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_shell": settings.SHELL_ENABLED is True,
"enable_slack_btn": settings.SLACK_CLIENT_ID is not None, "enable_slack_btn": settings.SLACK_CLIENT_ID is not None,
"enable_sms": settings.TWILIO_AUTH 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_telegram": settings.TELEGRAM_TOKEN is not None,
"enable_trello": settings.TRELLO_APP_KEY is not None, "enable_trello": settings.TRELLO_APP_KEY is not None,
"enable_whatsapp": settings.TWILIO_USE_WHATSAPP, "enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
@ -1819,21 +1821,82 @@ def add_spike(request, code):
return render(request, "integrations/add_spike.html", ctx) return render(request, "integrations/add_spike.html", ctx)
@require_setting("LINENOTIFY_CLIENT_ID")
@login_required @login_required
def add_linenotify(request, code): def add_linenotify(request, code):
project = _get_rw_project_for_user(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) 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

+ 3
- 0
hc/settings.py View File

@ -219,6 +219,9 @@ APPRISE_ENABLED = envbool("APPRISE_ENABLED", "False")
# Local shell commands # Local shell commands
SHELL_ENABLED = envbool("SHELL_ENABLED", "False") 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")): if os.path.exists(os.path.join(BASE_DIR, "hc/local_settings.py")):
from .local_settings import * from .local_settings import *


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

Before After
Width: 2030  |  Height: 1626  |  Size: 253 KiB

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

Before After
Width: 996  |  Height: 1472  |  Size: 121 KiB

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

Before After
Width: 1034  |  Height: 906  |  Size: 79 KiB

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

@ -230,6 +230,7 @@
</li> </li>
{% endif %} {% endif %}
{% if enable_linenotify %}
<li> <li>
<img src="{% static 'img/integrations/linenotify.png' %}" <img src="{% static 'img/integrations/linenotify.png' %}"
class="icon" alt="LINE Notify icon" /> class="icon" alt="LINE Notify icon" />
@ -238,6 +239,7 @@
<p>Receive a notification on LINE when a check goes up or down.</p> <p>Receive a notification on LINE when a check goes up or down.</p>
<a href="{% url 'hc-add-linenotify' project.code %}" class="btn btn-primary">Add Integration</a> <a href="{% url 'hc-add-linenotify' project.code %}" class="btn btn-primary">Add Integration</a>
</li> </li>
{% endif %}
{% if enable_matrix %} {% if enable_matrix %}
<li> <li>


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

@ -435,12 +435,14 @@
</div> </div>
{% endif %} {% endif %}
{% if enable_linenotify %}
<div class="col-lg-2 col-md-3 col-sm-4 col-xs-6"> <div class="col-lg-2 col-md-3 col-sm-4 col-xs-6">
<div class="integration"> <div class="integration">
<img src="{% static 'img/integrations/linenotify.png' %}" class="icon" alt="" /> <img src="{% static 'img/integrations/linenotify.png' %}" class="icon" alt="" />
<h3>LINE Notify<br><small>Chat</small></h3> <h3>LINE Notify<br><small>Chat</small></h3>
</div> </div>
</div> </div>
{% endif %}
{% if enable_matrix %} {% if enable_matrix %}
<div class="col-lg-2 col-md-3 col-sm-4 col-xs-6"> <div class="col-lg-2 col-md-3 col-sm-4 col-xs-6">


+ 22
- 96
templates/integrations/add_linenotify.html View File

@ -1,104 +1,30 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load humanize static hc_extras %} {% load humanize static hc_extras %}
{% block title %}LineNotify Integration for {% site_name %}{% endblock %}
{% block title %}LINE Notify Integration for {{ site_name }}{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-sm-12">
<h1>LINE Notify</h1>
<p><a href="https://notify-bot.line.me/en/">LINE Notify</a>
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. </p>
<h2>Setup Guide</h2>
<div class="row ai-step">
<div class="col-sm-6">
<span class="step-no"></span>
<p>
Log into your LINE account,
go to <strong>My Page</strong> in the upper right corner.
In the <strong>My Page</strong>, scroll to the bottom of the page,
and click on <strong>Generate token</strong> button.
</p>
</div>
<div class="col-sm-6">
<div class="marker-wrap">
<span style="left: 77%; top: 14%;" class="marker"></span>
<img
class="ai-guide-screenshot"
alt="Screenshot"
src="{% static 'img/integrations/setup_linenotify_1.png' %}" />
</div>
</div>
</div>
<div class="row ai-step">
<div class="col-sm-6">
<span class="step-no"></span>
<p>
It will pop up the <strong>Generate token</strong> Form.
Fill out the details, and click the button.
</p>
</div>
<div class="col-sm-6">
<img
class="ai-guide-screenshot"
alt="Screenshot"
src="{% static 'img/integrations/setup_linenotify_2.png' %}">
</div>
</div>
<div class="row ai-step">
<div class="col-sm-6">
<span class="step-no"></span>
<p>
Copy the displayed <strong>Token</strong> and paste it
in the form below.
Save the integration, and it's done!
</p>
</div>
<div class="col-sm-6">
<img
class="ai-guide-screenshot"
alt="Screenshot"
src="{% static 'img/integrations/setup_linenotify_3.png' %}">
</div>
</div>
<h2>Integration Settings</h2>
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="form-group {{ form.value.css_classes }}">
<label for="api-token" class="col-sm-2 control-label">API Token</label>
<div class="col-sm-10">
<input
id="api-token"
type="text"
class="form-control"
name="token"
placeholder=""
value="{{ form.token.value|default:"" }}">
{% if form.token.errors %}
<div class="help-block">
{{ form.token.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 class="col-sm-12">
<h1>LINE Notify</h1>
<div class="jumbotron">
<p>
With the <a href="https://notify-bot.line.me/en/">LINE Notify</a>
integration, {{ site_name }} will send
a notification to your selected LINE chat 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-lg btn-default">
<img class="ai-icon" src="{% static 'img/integrations/linenotify.png' %}" alt="LINE Notify" />
Connect LINE Notify
</a>
</form>
</div>
</div>
</div> </div>
{% endblock %}
{% endblock %}

Loading…
Cancel
Save