|
|
- from collections import Counter
- from datetime import datetime, timedelta as td
- from itertools import tee
- import json
-
- from croniter import croniter
- from django.conf import settings
- from django.contrib import messages
- from django.contrib.auth.decorators import login_required
- from django.core import signing
- from django.db.models import Count
- from django.http import (Http404, HttpResponse, HttpResponseBadRequest,
- HttpResponseForbidden)
- from django.shortcuts import get_object_or_404, redirect, render
- from django.template.loader import render_to_string
- from django.urls import reverse
- from django.utils import timezone
- from django.utils.crypto import get_random_string
- from django.utils.six.moves.urllib.parse import urlencode
- from django.views.decorators.csrf import csrf_exempt
- from django.views.decorators.http import require_POST
- from hc.api.decorators import uuid_or_400
- from hc.api.models import (DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check,
- Ping, Notification)
- from hc.api.transports import Telegram
- from hc.front.forms import (AddWebhookForm, NameTagsForm,
- TimeoutForm, AddUrlForm, AddPdForm, AddEmailForm,
- AddOpsGenieForm, CronForm, AddSmsForm)
- from hc.front.schemas import telegram_callback
- from hc.lib import jsonschema
- from pytz import all_timezones
- from pytz.exceptions import UnknownTimeZoneError
- import requests
-
-
- # from itertools recipes:
- def pairwise(iterable):
- "s -> (s0,s1), (s1,s2), (s2, s3), ..."
- a, b = tee(iterable)
- next(b, None)
- return zip(a, b)
-
-
- @login_required
- def my_checks(request):
- q = Check.objects.filter(user=request.team.user).order_by("created")
- checks = list(q)
-
- counter = Counter()
- down_tags, grace_tags = set(), set()
- for check in checks:
- status = check.get_status()
- for tag in check.tags_list():
- if tag == "":
- continue
-
- counter[tag] += 1
-
- if status == "down":
- down_tags.add(tag)
- elif check.in_grace_period():
- grace_tags.add(tag)
-
- can_add_more = len(checks) < request.team.check_limit
-
- ctx = {
- "page": "checks",
- "checks": checks,
- "now": timezone.now(),
- "tags": counter.most_common(),
- "down_tags": down_tags,
- "grace_tags": grace_tags,
- "ping_endpoint": settings.PING_ENDPOINT,
- "timezones": all_timezones,
- "can_add_more": can_add_more
- }
-
- return render(request, "front/my_checks.html", ctx)
-
-
- def _welcome_check(request):
- check = None
- if "welcome_code" in request.session:
- code = request.session["welcome_code"]
- check = Check.objects.filter(code=code).first()
-
- if check is None:
- check = Check()
- check.save()
- request.session["welcome_code"] = str(check.code)
-
- return check
-
-
- def index(request):
- if request.user.is_authenticated:
- return redirect("hc-checks")
-
- check = _welcome_check(request)
-
- ctx = {
- "page": "welcome",
- "check": check,
- "ping_url": check.url(),
- "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
- "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
- "enable_discord": settings.DISCORD_CLIENT_ID is not None,
- "enable_telegram": settings.TELEGRAM_TOKEN is not None,
- "enable_sms": settings.TWILIO_AUTH is not None,
- "registration_open": settings.REGISTRATION_OPEN
- }
-
- return render(request, "front/welcome.html", ctx)
-
-
- def docs(request):
- check = _welcome_check(request)
-
- ctx = {
- "page": "docs",
- "section": "home",
- "ping_endpoint": settings.PING_ENDPOINT,
- "check": check,
- "ping_url": check.url()
- }
-
- return render(request, "front/docs.html", ctx)
-
-
- def docs_api(request):
- ctx = {
- "page": "docs",
- "section": "api",
- "SITE_ROOT": settings.SITE_ROOT,
- "PING_ENDPOINT": settings.PING_ENDPOINT,
- "default_timeout": int(DEFAULT_TIMEOUT.total_seconds()),
- "default_grace": int(DEFAULT_GRACE.total_seconds())
- }
-
- return render(request, "front/docs_api.html", ctx)
-
- def docs_cron(request):
- ctx = {"page": "docs", "section": "cron"}
- return render(request, "front/docs_cron.html", ctx)
-
-
- def about(request):
- return render(request, "front/about.html", {"page": "about"})
-
-
- @require_POST
- @login_required
- def add_check(request):
- num_checks = Check.objects.filter(user=request.team.user).count()
- if num_checks >= request.team.check_limit:
- return HttpResponseBadRequest()
-
- check = Check(user=request.team.user)
- check.save()
-
- check.assign_all_channels()
-
- return redirect("hc-checks")
-
-
- @require_POST
- @login_required
- @uuid_or_400
- def update_name(request, code):
- check = get_object_or_404(Check, code=code)
- if check.user_id != request.team.user.id:
- return HttpResponseForbidden()
-
- form = NameTagsForm(request.POST)
- if form.is_valid():
- check.name = form.cleaned_data["name"]
- check.tags = form.cleaned_data["tags"]
- check.save()
-
- return redirect("hc-checks")
-
-
- @require_POST
- @login_required
- @uuid_or_400
- def update_timeout(request, code):
- check = get_object_or_404(Check, code=code)
- if check.user != request.team.user:
- return HttpResponseForbidden()
-
- kind = request.POST.get("kind")
- if kind == "simple":
- form = TimeoutForm(request.POST)
- if not form.is_valid():
- return HttpResponseBadRequest()
-
- check.kind = "simple"
- check.timeout = td(seconds=form.cleaned_data["timeout"])
- check.grace = td(seconds=form.cleaned_data["grace"])
- elif kind == "cron":
- form = CronForm(request.POST)
- if not form.is_valid():
- return HttpResponseBadRequest()
-
- check.kind = "cron"
- check.schedule = form.cleaned_data["schedule"]
- check.tz = form.cleaned_data["tz"]
- check.grace = td(minutes=form.cleaned_data["grace"])
-
- if check.last_ping:
- check.alert_after = check.get_alert_after()
-
- check.save()
- return redirect("hc-checks")
-
-
- @require_POST
- def cron_preview(request):
- schedule = request.POST.get("schedule")
- tz = request.POST.get("tz")
- ctx = {"tz": tz, "dates": []}
- try:
- with timezone.override(tz):
- now_naive = timezone.make_naive(timezone.now())
- it = croniter(schedule, now_naive)
- for i in range(0, 6):
- naive = it.get_next(datetime)
- aware = timezone.make_aware(naive)
- ctx["dates"].append((naive, aware))
- except UnknownTimeZoneError:
- ctx["bad_tz"] = True
- except:
- ctx["bad_schedule"] = True
-
- return render(request, "front/cron_preview.html", ctx)
-
-
- @require_POST
- def last_ping(request, code):
- if not request.user.is_authenticated:
- return HttpResponseForbidden()
-
- check = get_object_or_404(Check, code=code)
- if check.user_id != request.team.user.id:
- return HttpResponseForbidden()
-
- ping = Ping.objects.filter(owner=check).latest("created")
-
- ctx = {
- "check": check,
- "ping": ping
- }
-
- return render(request, "front/last_ping.html", ctx)
-
-
- @require_POST
- @login_required
- @uuid_or_400
- def pause(request, code):
- check = get_object_or_404(Check, code=code)
- if check.user_id != request.team.user.id:
- return HttpResponseForbidden()
-
- check.status = "paused"
- check.save()
-
- return redirect("hc-checks")
-
-
- @require_POST
- @login_required
- @uuid_or_400
- def remove_check(request, code):
- check = get_object_or_404(Check, code=code)
- if check.user != request.team.user:
- return HttpResponseForbidden()
-
- check.delete()
-
- return redirect("hc-checks")
-
-
- @login_required
- @uuid_or_400
- def log(request, code):
- check = get_object_or_404(Check, code=code)
- if check.user != request.team.user:
- return HttpResponseForbidden()
-
- limit = request.team.ping_log_limit
- pings = Ping.objects.filter(owner=check).order_by("-id")[:limit + 1]
- pings = list(pings)
-
- num_pings = len(pings)
- pings = pings[:limit]
-
- alerts = []
- if len(pings):
- cutoff = pings[-1].created
- alerts = Notification.objects \
- .select_related("channel") \
- .filter(owner=check, check_status="down", created__gt=cutoff)
-
- events = pings + list(alerts)
- events.sort(key=lambda el: el.created, reverse=True)
-
- ctx = {
- "check": check,
- "events": events,
- "num_pings": min(num_pings, limit),
- "limit": limit,
- "show_limit_notice": num_pings > limit and settings.USE_PAYMENTS
- }
-
- return render(request, "front/log.html", ctx)
-
-
- @login_required
- def channels(request):
- if request.method == "POST":
- code = request.POST["channel"]
- try:
- channel = Channel.objects.get(code=code)
- except Channel.DoesNotExist:
- return HttpResponseBadRequest()
- if channel.user_id != request.team.user.id:
- return HttpResponseForbidden()
-
- new_checks = []
- for key in request.POST:
- if key.startswith("check-"):
- code = key[6:]
- try:
- check = Check.objects.get(code=code)
- except Check.DoesNotExist:
- return HttpResponseBadRequest()
- if check.user_id != request.team.user.id:
- return HttpResponseForbidden()
- new_checks.append(check)
-
- channel.checks.set(new_checks)
- return redirect("hc-channels")
-
- channels = Channel.objects.filter(user=request.team.user)
- channels = channels.order_by("created")
- channels = channels.annotate(n_checks=Count("checks"))
-
- num_checks = Check.objects.filter(user=request.team.user).count()
-
- ctx = {
- "page": "channels",
- "profile": request.team,
- "channels": channels,
- "num_checks": num_checks,
- "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
- "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
- "enable_discord": settings.DISCORD_CLIENT_ID is not None,
- "enable_telegram": settings.TELEGRAM_TOKEN is not None,
- "enable_sms": settings.TWILIO_AUTH is not None
- }
- return render(request, "front/channels.html", ctx)
-
-
- @login_required
- @uuid_or_400
- def channel_checks(request, code):
- channel = get_object_or_404(Channel, code=code)
- if channel.user_id != request.team.user.id:
- return HttpResponseForbidden()
-
- assigned = set(channel.checks.values_list('code', flat=True).distinct())
- checks = Check.objects.filter(user=request.team.user).order_by("created")
-
- ctx = {
- "checks": checks,
- "assigned": assigned,
- "channel": channel
- }
-
- return render(request, "front/channel_checks.html", ctx)
-
-
- @uuid_or_400
- def verify_email(request, code, token):
- channel = get_object_or_404(Channel, code=code)
- if channel.make_token() == token:
- channel.email_verified = True
- channel.save()
- return render(request, "front/verify_email_success.html")
-
- return render(request, "bad_link.html")
-
-
- @uuid_or_400
- def unsubscribe_email(request, code, token):
- channel = get_object_or_404(Channel, code=code)
- if channel.make_token() != token:
- return render(request, "bad_link.html")
-
- if channel.kind != "email":
- return HttpResponseBadRequest()
-
- channel.delete()
- return render(request, "front/unsubscribe_success.html")
-
-
- @require_POST
- @login_required
- @uuid_or_400
- def remove_channel(request, code):
- # user may refresh the page during POST and cause two deletion attempts
- channel = Channel.objects.filter(code=code).first()
- if channel:
- if channel.user != request.team.user:
- return HttpResponseForbidden()
- channel.delete()
-
- return redirect("hc-channels")
-
-
- @login_required
- def add_email(request):
- if request.method == "POST":
- form = AddEmailForm(request.POST)
- if form.is_valid():
- channel = Channel(user=request.team.user, kind="email")
- channel.value = form.cleaned_data["value"]
- channel.save()
-
- channel.assign_all_checks()
- channel.send_verify_link()
- return redirect("hc-channels")
- else:
- form = AddEmailForm()
-
- ctx = {"page": "channels", "form": form}
- return render(request, "integrations/add_email.html", ctx)
-
-
- @login_required
- def add_webhook(request):
- if request.method == "POST":
- form = AddWebhookForm(request.POST)
- if form.is_valid():
- channel = Channel(user=request.team.user, kind="webhook")
- channel.value = form.get_value()
- channel.save()
-
- channel.assign_all_checks()
- return redirect("hc-channels")
- else:
- form = AddWebhookForm()
-
- ctx = {
- "page": "channels",
- "form": form,
- "now": timezone.now().replace(microsecond=0).isoformat()
- }
- return render(request, "integrations/add_webhook.html", ctx)
-
-
- @login_required
- def add_pd(request):
- if request.method == "POST":
- form = AddPdForm(request.POST)
- if form.is_valid():
- channel = Channel(user=request.team.user, kind="pd")
- channel.value = form.cleaned_data["value"]
- channel.save()
-
- channel.assign_all_checks()
- return redirect("hc-channels")
- else:
- form = AddPdForm()
-
- ctx = {"page": "channels", "form": form}
- return render(request, "integrations/add_pd.html", ctx)
-
-
- def _prepare_state(request, session_key):
- state = get_random_string()
- request.session[session_key] = state
- return state
-
-
- def _get_validated_code(request, session_key):
- if session_key not in request.session:
- return None
-
- session_state = request.session.pop(session_key)
- request_state = request.GET.get("state")
- if session_state is None or session_state != request_state:
- return None
-
- return request.GET.get("code")
-
-
- def add_slack(request):
- if not settings.SLACK_CLIENT_ID and not request.user.is_authenticated:
- return redirect("hc-login")
-
- if request.method == "POST":
- form = AddUrlForm(request.POST)
- if form.is_valid():
- channel = Channel(user=request.team.user, kind="slack")
- channel.value = form.cleaned_data["value"]
- channel.save()
-
- channel.assign_all_checks()
- return redirect("hc-channels")
- else:
- form = AddUrlForm()
-
- ctx = {
- "page": "channels",
- "form": form,
- "slack_client_id": settings.SLACK_CLIENT_ID
- }
-
- if settings.SLACK_CLIENT_ID:
- ctx["state"] = _prepare_state(request, "slack")
-
- return render(request, "integrations/add_slack.html", ctx)
-
-
- @login_required
- def add_slack_btn(request):
- code = _get_validated_code(request, "slack")
- if code is None:
- return HttpResponseBadRequest()
-
- result = requests.post("https://slack.com/api/oauth.access", {
- "client_id": settings.SLACK_CLIENT_ID,
- "client_secret": settings.SLACK_CLIENT_SECRET,
- "code": code
- })
-
- doc = result.json()
- if doc.get("ok"):
- channel = Channel()
- channel.user = request.team.user
- channel.kind = "slack"
- channel.value = result.text
- channel.save()
- channel.assign_all_checks()
- messages.success(request, "The Slack integration has been added!")
- else:
- s = doc.get("error")
- messages.warning(request, "Error message from slack: %s" % s)
-
- return redirect("hc-channels")
-
-
- @login_required
- def add_hipchat(request):
- if request.method == "POST":
- form = AddUrlForm(request.POST)
- if form.is_valid():
- channel = Channel(user=request.team.user, kind="hipchat")
- channel.value = form.cleaned_data["value"]
- channel.save()
-
- channel.assign_all_checks()
- return redirect("hc-channels")
- else:
- form = AddUrlForm()
-
- ctx = {"page": "channels", "form": form}
- return render(request, "integrations/add_hipchat.html", ctx)
-
-
- @login_required
- def add_pushbullet(request):
- if settings.PUSHBULLET_CLIENT_ID is None:
- raise Http404("pushbullet integration is not available")
-
- if "code" in request.GET:
- code = _get_validated_code(request, "pushbullet")
- if code is None:
- return HttpResponseBadRequest()
-
- result = requests.post("https://api.pushbullet.com/oauth2/token", {
- "client_id": settings.PUSHBULLET_CLIENT_ID,
- "client_secret": settings.PUSHBULLET_CLIENT_SECRET,
- "code": code,
- "grant_type": "authorization_code"
- })
-
- doc = result.json()
- if "access_token" in doc:
- channel = Channel(kind="pushbullet")
- channel.user = request.team.user
- channel.value = doc["access_token"]
- channel.save()
- channel.assign_all_checks()
- messages.success(request,
- "The Pushbullet integration has been added!")
- else:
- messages.warning(request, "Something went wrong")
-
- return redirect("hc-channels")
-
- redirect_uri = settings.SITE_ROOT + reverse("hc-add-pushbullet")
- authorize_url = "https://www.pushbullet.com/authorize?" + urlencode({
- "client_id": settings.PUSHBULLET_CLIENT_ID,
- "redirect_uri": redirect_uri,
- "response_type": "code",
- "state": _prepare_state(request, "pushbullet")
- })
-
- ctx = {
- "page": "channels",
- "authorize_url": authorize_url
- }
- return render(request, "integrations/add_pushbullet.html", ctx)
-
-
- @login_required
- def add_discord(request):
- if settings.DISCORD_CLIENT_ID is None:
- raise Http404("discord integration is not available")
-
- redirect_uri = settings.SITE_ROOT + reverse("hc-add-discord")
- if "code" in request.GET:
- code = _get_validated_code(request, "discord")
- if code is None:
- return HttpResponseBadRequest()
-
- result = requests.post("https://discordapp.com/api/oauth2/token", {
- "client_id": settings.DISCORD_CLIENT_ID,
- "client_secret": settings.DISCORD_CLIENT_SECRET,
- "code": code,
- "grant_type": "authorization_code",
- "redirect_uri": redirect_uri
- })
-
- doc = result.json()
- if "access_token" in doc:
- channel = Channel(kind="discord")
- channel.user = request.team.user
- channel.value = result.text
- channel.save()
- channel.assign_all_checks()
- messages.success(request,
- "The Discord integration has been added!")
- else:
- messages.warning(request, "Something went wrong")
-
- return redirect("hc-channels")
-
- auth_url = "https://discordapp.com/api/oauth2/authorize?" + urlencode({
- "client_id": settings.DISCORD_CLIENT_ID,
- "scope": "webhook.incoming",
- "redirect_uri": redirect_uri,
- "response_type": "code",
- "state": _prepare_state(request, "discord")
- })
-
- ctx = {
- "page": "channels",
- "authorize_url": auth_url
- }
-
- return render(request, "integrations/add_discord.html", ctx)
-
-
- @login_required
- def add_pushover(request):
- if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None:
- raise Http404("pushover integration is not available")
-
- if request.method == "POST":
- # Initiate the subscription
- nonce = get_random_string()
- request.session["po_nonce"] = nonce
-
- failure_url = settings.SITE_ROOT + reverse("hc-channels")
- success_url = settings.SITE_ROOT + reverse("hc-add-pushover") + "?" + urlencode({
- "nonce": nonce,
- "prio": request.POST.get("po_priority", "0"),
- })
- subscription_url = settings.PUSHOVER_SUBSCRIPTION_URL + "?" + urlencode({
- "success": success_url,
- "failure": failure_url,
- })
-
- return redirect(subscription_url)
-
- # Handle successful subscriptions
- if "pushover_user_key" in request.GET:
- if "nonce" not in request.GET or "prio" not in request.GET:
- return HttpResponseBadRequest()
-
- # Validate nonce
- if request.GET["nonce"] != request.session.get("po_nonce"):
- return HttpResponseForbidden()
-
- # Validate priority
- if request.GET["prio"] not in ("-2", "-1", "0", "1", "2"):
- return HttpResponseBadRequest()
-
- # All looks well--
- del request.session["po_nonce"]
-
- if request.GET.get("pushover_unsubscribed") == "1":
- # Unsubscription: delete all Pushover channels for this user
- Channel.objects.filter(user=request.user, kind="po").delete()
- return redirect("hc-channels")
- else:
- # Subscription
- user_key = request.GET["pushover_user_key"]
- priority = int(request.GET["prio"])
-
- channel = Channel(user=request.team.user, kind="po")
- channel.value = "%s|%d" % (user_key, priority)
- channel.save()
- channel.assign_all_checks()
- return redirect("hc-channels")
-
- # Show Integration Settings form
- ctx = {
- "page": "channels",
- "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY),
- "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION),
- }
- return render(request, "integrations/add_pushover.html", ctx)
-
-
- @login_required
- def add_opsgenie(request):
- if request.method == "POST":
- form = AddOpsGenieForm(request.POST)
- if form.is_valid():
- channel = Channel(user=request.team.user, kind="opsgenie")
- channel.value = form.cleaned_data["value"]
- channel.save()
-
- channel.assign_all_checks()
- return redirect("hc-channels")
- else:
- form = AddUrlForm()
-
- ctx = {"page": "channels", "form": form}
- return render(request, "integrations/add_opsgenie.html", ctx)
-
-
- @login_required
- def add_victorops(request):
- if request.method == "POST":
- form = AddUrlForm(request.POST)
- if form.is_valid():
- channel = Channel(user=request.team.user, kind="victorops")
- channel.value = form.cleaned_data["value"]
- channel.save()
-
- channel.assign_all_checks()
- return redirect("hc-channels")
- else:
- form = AddUrlForm()
-
- ctx = {"page": "channels", "form": form}
- return render(request, "integrations/add_victorops.html", ctx)
-
-
- @csrf_exempt
- @require_POST
- def telegram_bot(request):
- try:
- doc = json.loads(request.body.decode("utf-8"))
- jsonschema.validate(doc, telegram_callback)
- except ValueError:
- return HttpResponseBadRequest()
- except jsonschema.ValidationError:
- return HttpResponseBadRequest()
-
- if "/start" not in doc["message"]["text"]:
- return HttpResponse()
-
- chat = doc["message"]["chat"]
- name = max(chat.get("title", ""), chat.get("username", ""))
-
- invite = render_to_string("integrations/telegram_invite.html", {
- "qs": signing.dumps((chat["id"], chat["type"], name))
- })
-
- Telegram.send(chat["id"], invite)
- return HttpResponse()
-
-
- @login_required
- def add_telegram(request):
- chat_id, chat_type, chat_name = None, None, None
- qs = request.META["QUERY_STRING"]
- if qs:
- chat_id, chat_type, chat_name = signing.loads(qs, max_age=600)
-
- if request.method == "POST":
- channel = Channel(user=request.team.user, kind="telegram")
- channel.value = json.dumps({
- "id": chat_id,
- "type": chat_type,
- "name": chat_name
- })
- channel.save()
-
- channel.assign_all_checks()
- messages.success(request, "The Telegram integration has been added!")
- return redirect("hc-channels")
-
- ctx = {
- "chat_id": chat_id,
- "chat_type": chat_type,
- "chat_name": chat_name,
- "bot_name": settings.TELEGRAM_BOT_NAME
- }
-
- return render(request, "integrations/add_telegram.html", ctx)
-
-
- @login_required
- def add_sms(request):
- if settings.TWILIO_AUTH is None:
- raise Http404("sms integration is not available")
-
- if request.method == "POST":
- form = AddSmsForm(request.POST)
- if form.is_valid():
- channel = Channel(user=request.team.user, kind="sms")
- channel.value = form.cleaned_data["value"]
- channel.save()
-
- channel.assign_all_checks()
- return redirect("hc-channels")
- else:
- form = AddSmsForm()
-
- ctx = {
- "page": "channels",
- "form": form,
- "profile": request.team
- }
- return render(request, "integrations/add_sms.html", ctx)
-
-
- def privacy(request):
- return render(request, "front/privacy.html", {})
-
-
- def terms(request):
- return render(request, "front/terms.html", {})
|