You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2070 lines
64 KiB

6 years ago
9 years ago
9 years ago
8 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
8 years ago
9 years ago
10 years ago
10 years ago
10 years ago
6 years ago
10 years ago
10 years ago
6 years ago
9 years ago
6 years ago
9 years ago
9 years ago
9 years ago
9 years ago
8 years ago
  1. from datetime import datetime, timedelta as td
  2. import email
  3. import json
  4. import os
  5. import re
  6. from secrets import token_urlsafe
  7. from urllib.parse import urlencode
  8. from cron_descriptor import ExpressionDescriptor
  9. from croniter import croniter
  10. from django.conf import settings
  11. from django.contrib import messages
  12. from django.contrib.auth.decorators import login_required
  13. from django.core import signing
  14. from django.core.exceptions import PermissionDenied
  15. from django.db.models import Count, F
  16. from django.http import (
  17. Http404,
  18. HttpResponse,
  19. HttpResponseBadRequest,
  20. HttpResponseForbidden,
  21. JsonResponse,
  22. )
  23. from django.shortcuts import get_object_or_404, redirect, render
  24. from django.template.loader import get_template, render_to_string
  25. from django.urls import reverse
  26. from django.utils import timezone
  27. from django.views.decorators.csrf import csrf_exempt
  28. from django.views.decorators.http import require_POST
  29. from hc.accounts.models import Project, Member
  30. from hc.api.models import (
  31. DEFAULT_GRACE,
  32. DEFAULT_TIMEOUT,
  33. MAX_DELTA,
  34. Channel,
  35. Check,
  36. Ping,
  37. Notification,
  38. )
  39. from hc.api.transports import Telegram
  40. from hc.front.decorators import require_setting
  41. from hc.front import forms
  42. from hc.front.schemas import telegram_callback
  43. from hc.front.templatetags.hc_extras import (
  44. num_down_title,
  45. down_title,
  46. sortchecks,
  47. site_hostname,
  48. site_scheme,
  49. )
  50. from hc.lib import jsonschema
  51. from hc.lib.badges import get_badge_url
  52. import pytz
  53. from pytz.exceptions import UnknownTimeZoneError
  54. import requests
  55. VALID_SORT_VALUES = ("name", "-name", "last_ping", "-last_ping", "created")
  56. STATUS_TEXT_TMPL = get_template("front/log_status_text.html")
  57. LAST_PING_TMPL = get_template("front/last_ping_cell.html")
  58. EVENTS_TMPL = get_template("front/details_events.html")
  59. DOWNTIMES_TMPL = get_template("front/details_downtimes.html")
  60. def _tags_statuses(checks):
  61. tags, down, grace, num_down = {}, {}, {}, 0
  62. for check in checks:
  63. status = check.get_status()
  64. if status == "down":
  65. num_down += 1
  66. for tag in check.tags_list():
  67. down[tag] = "down"
  68. elif status == "grace":
  69. for tag in check.tags_list():
  70. grace[tag] = "grace"
  71. else:
  72. for tag in check.tags_list():
  73. tags[tag] = "up"
  74. tags.update(grace)
  75. tags.update(down)
  76. return tags, num_down
  77. def _get_check_for_user(request, code):
  78. """ Return specified check if current user has access to it. """
  79. assert request.user.is_authenticated
  80. check = get_object_or_404(Check.objects.select_related("project"), code=code)
  81. if request.user.is_superuser:
  82. return check, True
  83. if request.user.id == check.project.owner_id:
  84. return check, True
  85. membership = get_object_or_404(Member, project=check.project, user=request.user)
  86. return check, membership.rw
  87. def _get_rw_check_for_user(request, code):
  88. check, rw = _get_check_for_user(request, code)
  89. if not rw:
  90. raise PermissionDenied
  91. return check
  92. def _get_channel_for_user(request, code):
  93. """ Return specified channel if current user has access to it. """
  94. assert request.user.is_authenticated
  95. channel = get_object_or_404(Channel.objects.select_related("project"), code=code)
  96. if request.user.is_superuser:
  97. return channel, True
  98. if request.user.id == channel.project.owner_id:
  99. return channel, True
  100. membership = get_object_or_404(Member, project=channel.project, user=request.user)
  101. return channel, membership.rw
  102. def _get_rw_channel_for_user(request, code):
  103. channel, rw = _get_channel_for_user(request, code)
  104. if not rw:
  105. raise PermissionDenied
  106. return channel
  107. def _get_project_for_user(request, project_code):
  108. """ Check access, return (project, rw) tuple. """
  109. project = get_object_or_404(Project, code=project_code)
  110. if request.user.is_superuser:
  111. return project, True
  112. if request.user.id == project.owner_id:
  113. return project, True
  114. membership = get_object_or_404(Member, project=project, user=request.user)
  115. return project, membership.rw
  116. def _get_rw_project_for_user(request, project_code):
  117. """ Check access, return (project, rw) tuple. """
  118. project, rw = _get_project_for_user(request, project_code)
  119. if not rw:
  120. raise PermissionDenied
  121. return project
  122. def _refresh_last_active_date(profile):
  123. """ Update last_active_date if it is more than a day old. """
  124. now = timezone.now()
  125. if profile.last_active_date is None or (now - profile.last_active_date).days > 0:
  126. profile.last_active_date = now
  127. profile.save()
  128. @login_required
  129. def my_checks(request, code):
  130. _refresh_last_active_date(request.profile)
  131. project, rw = _get_project_for_user(request, code)
  132. if request.GET.get("sort") in VALID_SORT_VALUES:
  133. request.profile.sort = request.GET["sort"]
  134. request.profile.save()
  135. if request.session.get("last_project_id") != project.id:
  136. request.session["last_project_id"] = project.id
  137. q = Check.objects.filter(project=project)
  138. checks = list(q.prefetch_related("channel_set"))
  139. sortchecks(checks, request.profile.sort)
  140. tags_statuses, num_down = _tags_statuses(checks)
  141. pairs = list(tags_statuses.items())
  142. pairs.sort(key=lambda pair: pair[0].lower())
  143. channels = Channel.objects.filter(project=project)
  144. channels = list(channels.order_by("created"))
  145. hidden_checks = set()
  146. # Hide checks that don't match selected tags:
  147. selected_tags = set(request.GET.getlist("tag", []))
  148. if selected_tags:
  149. for check in checks:
  150. if not selected_tags.issubset(check.tags_list()):
  151. hidden_checks.add(check)
  152. # Hide checks that don't match the search string:
  153. search = request.GET.get("search", "")
  154. if search:
  155. for check in checks:
  156. search_key = "%s\n%s" % (check.name.lower(), check.code)
  157. if search not in search_key:
  158. hidden_checks.add(check)
  159. # Do we need to show the "Last Duration" header?
  160. show_last_duration = False
  161. for check in checks:
  162. if check.clamped_last_duration():
  163. show_last_duration = True
  164. break
  165. ctx = {
  166. "page": "checks",
  167. "rw": rw,
  168. "checks": checks,
  169. "channels": channels,
  170. "num_down": num_down,
  171. "tags": pairs,
  172. "ping_endpoint": settings.PING_ENDPOINT,
  173. "timezones": pytz.all_timezones,
  174. "project": project,
  175. "num_available": project.num_checks_available(),
  176. "sort": request.profile.sort,
  177. "selected_tags": selected_tags,
  178. "search": search,
  179. "hidden_checks": hidden_checks,
  180. "show_last_duration": show_last_duration,
  181. }
  182. return render(request, "front/my_checks.html", ctx)
  183. @login_required
  184. def status(request, code):
  185. _get_project_for_user(request, code)
  186. checks = list(Check.objects.filter(project__code=code))
  187. details = []
  188. for check in checks:
  189. ctx = {"check": check}
  190. details.append(
  191. {
  192. "code": str(check.code),
  193. "status": check.get_status(),
  194. "last_ping": LAST_PING_TMPL.render(ctx),
  195. "started": check.last_start is not None,
  196. }
  197. )
  198. tags_statuses, num_down = _tags_statuses(checks)
  199. return JsonResponse(
  200. {"details": details, "tags": tags_statuses, "title": num_down_title(num_down)}
  201. )
  202. @login_required
  203. @require_POST
  204. def switch_channel(request, code, channel_code):
  205. check = _get_rw_check_for_user(request, code)
  206. channel = get_object_or_404(Channel, code=channel_code)
  207. if channel.project_id != check.project_id:
  208. return HttpResponseBadRequest()
  209. if request.POST.get("state") == "on":
  210. channel.checks.add(check)
  211. else:
  212. channel.checks.remove(check)
  213. return HttpResponse()
  214. def index(request):
  215. if request.user.is_authenticated:
  216. project_ids = request.profile.projects().values("id")
  217. q = Project.objects.filter(id__in=project_ids)
  218. q = q.annotate(n_checks=Count("check", distinct=True))
  219. q = q.annotate(n_channels=Count("channel", distinct=True))
  220. q = q.annotate(owner_email=F("owner__email"))
  221. projects = list(q)
  222. # Primary sort key: projects with overall_status=down go first
  223. # Secondary sort key: project's name
  224. projects.sort(key=lambda p: (p.overall_status() != "down", p.name))
  225. ctx = {
  226. "page": "projects",
  227. "projects": projects,
  228. "last_project_id": request.session.get("last_project_id"),
  229. }
  230. return render(request, "front/projects.html", ctx)
  231. check = Check()
  232. ctx = {
  233. "page": "welcome",
  234. "check": check,
  235. "ping_url": check.url(),
  236. "enable_apprise": settings.APPRISE_ENABLED is True,
  237. "enable_call": settings.TWILIO_AUTH is not None,
  238. "enable_discord": settings.DISCORD_CLIENT_ID is not None,
  239. "enable_linenotify": settings.LINENOTIFY_CLIENT_ID is not None,
  240. "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
  241. "enable_mattermost": settings.MATTERMOST_ENABLED is True,
  242. "enable_msteams": settings.MSTEAMS_ENABLED is True,
  243. "enable_opsgenie": settings.OPSGENIE_ENABLED is True,
  244. "enable_pagertree": settings.PAGERTREE_ENABLED is True,
  245. "enable_pd": settings.PD_ENABLED is True,
  246. "enable_pdc": settings.PD_VENDOR_KEY is not None,
  247. "enable_prometheus": settings.PROMETHEUS_ENABLED is True,
  248. "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
  249. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
  250. "enable_shell": settings.SHELL_ENABLED is True,
  251. "enable_signal": settings.SIGNAL_CLI_ENABLED is True,
  252. "enable_slack": settings.SLACK_ENABLED is True,
  253. "enable_slack_btn": settings.SLACK_CLIENT_ID is not None,
  254. "enable_sms": settings.TWILIO_AUTH is not None,
  255. "enable_spike": settings.SPIKE_ENABLED is True,
  256. "enable_telegram": settings.TELEGRAM_TOKEN is not None,
  257. "enable_trello": settings.TRELLO_APP_KEY is not None,
  258. "enable_victorops": settings.VICTOROPS_ENABLED is True,
  259. "enable_webhooks": settings.WEBHOOKS_ENABLED is True,
  260. "enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
  261. "enable_zulip": settings.ZULIP_ENABLED is True,
  262. "registration_open": settings.REGISTRATION_OPEN,
  263. }
  264. return render(request, "front/welcome.html", ctx)
  265. def dashboard(request):
  266. return render(request, "front/dashboard.html", {})
  267. def serve_doc(request, doc="introduction"):
  268. # Filenames in /templates/docs/ consist of lowercase letters and underscores,
  269. # -- make sure we don't accept anything else
  270. if not re.match(r"^[a-z_]+$", doc):
  271. raise Http404("not found")
  272. path = os.path.join(settings.BASE_DIR, "templates/docs", doc + ".html")
  273. if not os.path.exists(path):
  274. raise Http404("not found")
  275. content = open(path, "r", encoding="utf-8").read()
  276. if not doc.startswith("self_hosted"):
  277. replaces = {
  278. "{{ default_timeout }}": str(int(DEFAULT_TIMEOUT.total_seconds())),
  279. "{{ default_grace }}": str(int(DEFAULT_GRACE.total_seconds())),
  280. "SITE_NAME": settings.SITE_NAME,
  281. "SITE_ROOT": settings.SITE_ROOT,
  282. "SITE_HOSTNAME": site_hostname(),
  283. "SITE_SCHEME": site_scheme(),
  284. "PING_ENDPOINT": settings.PING_ENDPOINT,
  285. "PING_URL": settings.PING_ENDPOINT + "your-uuid-here",
  286. "IMG_URL": os.path.join(settings.STATIC_URL, "img/docs"),
  287. }
  288. for placeholder, value in replaces.items():
  289. content = content.replace(placeholder, value)
  290. ctx = {
  291. "page": "docs",
  292. "section": doc,
  293. "content": content,
  294. "first_line": content.split("\n")[0],
  295. }
  296. return render(request, "front/docs_single.html", ctx)
  297. def docs_cron(request):
  298. return render(request, "front/docs_cron.html", {})
  299. @require_POST
  300. @login_required
  301. def add_check(request, code):
  302. project = _get_rw_project_for_user(request, code)
  303. if project.num_checks_available() <= 0:
  304. return HttpResponseBadRequest()
  305. check = Check(project=project)
  306. check.save()
  307. check.assign_all_channels()
  308. url = reverse("hc-details", args=[check.code])
  309. return redirect(url + "?new")
  310. @require_POST
  311. @login_required
  312. def update_name(request, code):
  313. check = _get_rw_check_for_user(request, code)
  314. form = forms.NameTagsForm(request.POST)
  315. if form.is_valid():
  316. check.name = form.cleaned_data["name"]
  317. check.tags = form.cleaned_data["tags"]
  318. check.desc = form.cleaned_data["desc"]
  319. check.save()
  320. if "/details/" in request.META.get("HTTP_REFERER", ""):
  321. return redirect("hc-details", code)
  322. return redirect("hc-checks", check.project.code)
  323. @require_POST
  324. @login_required
  325. def filtering_rules(request, code):
  326. check = _get_rw_check_for_user(request, code)
  327. form = forms.FilteringRulesForm(request.POST)
  328. if form.is_valid():
  329. check.subject = form.cleaned_data["subject"]
  330. check.subject_fail = form.cleaned_data["subject_fail"]
  331. check.methods = form.cleaned_data["methods"]
  332. check.manual_resume = form.cleaned_data["manual_resume"]
  333. check.save()
  334. return redirect("hc-details", code)
  335. @require_POST
  336. @login_required
  337. def update_timeout(request, code):
  338. check = _get_rw_check_for_user(request, code)
  339. kind = request.POST.get("kind")
  340. if kind == "simple":
  341. form = forms.TimeoutForm(request.POST)
  342. if not form.is_valid():
  343. return HttpResponseBadRequest()
  344. check.kind = "simple"
  345. check.timeout = form.cleaned_data["timeout"]
  346. check.grace = form.cleaned_data["grace"]
  347. elif kind == "cron":
  348. form = forms.CronForm(request.POST)
  349. if not form.is_valid():
  350. return HttpResponseBadRequest()
  351. check.kind = "cron"
  352. check.schedule = form.cleaned_data["schedule"]
  353. check.tz = form.cleaned_data["tz"]
  354. check.grace = td(minutes=form.cleaned_data["grace"])
  355. check.alert_after = check.going_down_after()
  356. if check.status == "up" and check.alert_after < timezone.now():
  357. # Checks can flip from "up" to "down" state as a result of changing check's
  358. # schedule. We don't want to send notifications when changing schedule
  359. # interactively in the web UI. So we update the `alert_after` and `status`
  360. # fields here the same way as `sendalerts` would do, but without sending
  361. # an actual alert:
  362. check.alert_after = None
  363. check.status = "down"
  364. check.save()
  365. if "/details/" in request.META.get("HTTP_REFERER", ""):
  366. return redirect("hc-details", code)
  367. return redirect("hc-checks", check.project.code)
  368. @require_POST
  369. def cron_preview(request):
  370. schedule = request.POST.get("schedule", "")
  371. tz = request.POST.get("tz")
  372. ctx = {"tz": tz, "dates": []}
  373. try:
  374. zone = pytz.timezone(tz)
  375. now_local = timezone.localtime(timezone.now(), zone)
  376. if len(schedule.split()) != 5:
  377. raise ValueError()
  378. it = croniter(schedule, now_local)
  379. for i in range(0, 6):
  380. ctx["dates"].append(it.get_next(datetime))
  381. except UnknownTimeZoneError:
  382. ctx["bad_tz"] = True
  383. except:
  384. ctx["bad_schedule"] = True
  385. if ctx["dates"]:
  386. try:
  387. descriptor = ExpressionDescriptor(schedule, use_24hour_time_format=True)
  388. ctx["desc"] = descriptor.get_description()
  389. except:
  390. # We assume the schedule is valid if croniter accepts it.
  391. # If cron-descriptor throws an exception, don't show the description
  392. # to the user.
  393. pass
  394. return render(request, "front/cron_preview.html", ctx)
  395. @login_required
  396. def ping_details(request, code, n=None):
  397. check, rw = _get_check_for_user(request, code)
  398. q = Ping.objects.filter(owner=check)
  399. if n:
  400. q = q.filter(n=n)
  401. try:
  402. ping = q.latest("created")
  403. except Ping.DoesNotExist:
  404. return render(request, "front/ping_details_not_found.html")
  405. ctx = {"check": check, "ping": ping, "plain": None, "html": None}
  406. if ping.scheme == "email":
  407. parsed = email.message_from_string(ping.body, policy=email.policy.SMTP)
  408. ctx["subject"] = parsed.get("subject", "")
  409. plain_mime_part = parsed.get_body(("plain",))
  410. if plain_mime_part:
  411. ctx["plain"] = plain_mime_part.get_content()
  412. html_mime_part = parsed.get_body(("html",))
  413. if html_mime_part:
  414. ctx["html"] = html_mime_part.get_content()
  415. return render(request, "front/ping_details.html", ctx)
  416. @require_POST
  417. @login_required
  418. def pause(request, code):
  419. check = _get_rw_check_for_user(request, code)
  420. check.status = "paused"
  421. check.last_start = None
  422. check.alert_after = None
  423. check.save()
  424. # After pausing a check we must check if all checks are up,
  425. # and Profile.next_nag_date needs to be cleared out:
  426. check.project.update_next_nag_dates()
  427. # Don't redirect after an AJAX request:
  428. if request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest":
  429. return HttpResponse()
  430. return redirect("hc-details", code)
  431. @require_POST
  432. @login_required
  433. def resume(request, code):
  434. check = _get_rw_check_for_user(request, code)
  435. check.status = "new"
  436. check.last_start = None
  437. check.last_ping = None
  438. check.alert_after = None
  439. check.save()
  440. return redirect("hc-details", code)
  441. @require_POST
  442. @login_required
  443. def remove_check(request, code):
  444. check = _get_rw_check_for_user(request, code)
  445. project = check.project
  446. check.delete()
  447. return redirect("hc-checks", project.code)
  448. def _get_events(check, limit):
  449. pings = Ping.objects.filter(owner=check).order_by("-id")[:limit]
  450. pings = list(pings)
  451. prev = None
  452. for ping in reversed(pings):
  453. if ping.kind != "start" and prev and prev.kind == "start":
  454. delta = ping.created - prev.created
  455. if delta < MAX_DELTA:
  456. setattr(ping, "delta", delta)
  457. prev = ping
  458. alerts = []
  459. if len(pings):
  460. cutoff = pings[-1].created
  461. alerts = Notification.objects.select_related("channel").filter(
  462. owner=check, check_status="down", created__gt=cutoff
  463. )
  464. events = pings + list(alerts)
  465. events.sort(key=lambda el: el.created, reverse=True)
  466. return events
  467. @login_required
  468. def log(request, code):
  469. check, rw = _get_check_for_user(request, code)
  470. limit = check.project.owner_profile.ping_log_limit
  471. ctx = {
  472. "project": check.project,
  473. "check": check,
  474. "events": _get_events(check, limit),
  475. "limit": limit,
  476. "show_limit_notice": check.n_pings > limit and settings.USE_PAYMENTS,
  477. }
  478. return render(request, "front/log.html", ctx)
  479. @login_required
  480. def details(request, code):
  481. _refresh_last_active_date(request.profile)
  482. check, rw = _get_check_for_user(request, code)
  483. channels = Channel.objects.filter(project=check.project)
  484. channels = list(channels.order_by("created"))
  485. all_tags = set()
  486. q = Check.objects.filter(project=check.project).exclude(tags="")
  487. for tags in q.values_list("tags", flat=True):
  488. all_tags.update(tags.split(" "))
  489. ctx = {
  490. "page": "details",
  491. "project": check.project,
  492. "check": check,
  493. "rw": rw,
  494. "channels": channels,
  495. "enabled_channels": list(check.channel_set.all()),
  496. "timezones": pytz.all_timezones,
  497. "downtimes": check.downtimes(months=3),
  498. "is_new": "new" in request.GET,
  499. "is_copied": "copied" in request.GET,
  500. "all_tags": " ".join(sorted(all_tags)),
  501. }
  502. return render(request, "front/details.html", ctx)
  503. @login_required
  504. def uncloak(request, unique_key):
  505. for check in request.profile.checks_from_all_projects().only("code"):
  506. if check.unique_key == unique_key:
  507. return redirect("hc-details", check.code)
  508. raise Http404("not found")
  509. @login_required
  510. def transfer(request, code):
  511. check = _get_rw_check_for_user(request, code)
  512. if request.method == "POST":
  513. target_project = _get_rw_project_for_user(request, request.POST["project"])
  514. if target_project.num_checks_available() <= 0:
  515. return HttpResponseBadRequest()
  516. check.project = target_project
  517. check.save()
  518. check.assign_all_channels()
  519. messages.success(request, "Check transferred successfully!")
  520. return redirect("hc-details", code)
  521. ctx = {"check": check}
  522. return render(request, "front/transfer_modal.html", ctx)
  523. @require_POST
  524. @login_required
  525. def copy(request, code):
  526. check = _get_rw_check_for_user(request, code)
  527. if check.project.num_checks_available() <= 0:
  528. return HttpResponseBadRequest()
  529. new_name = check.name + " (copy)"
  530. # Make sure we don't exceed the 100 character db field limit:
  531. if len(new_name) > 100:
  532. new_name = check.name[:90] + "... (copy)"
  533. copied = Check(project=check.project)
  534. copied.name = new_name
  535. copied.desc, copied.tags = check.desc, check.tags
  536. copied.subject, copied.subject_fail = check.subject, check.subject_fail
  537. copied.methods = check.methods
  538. copied.manual_resume = check.manual_resume
  539. copied.kind = check.kind
  540. copied.timeout, copied.grace = check.timeout, check.grace
  541. copied.schedule, copied.tz = check.schedule, check.tz
  542. copied.save()
  543. copied.channel_set.add(*check.channel_set.all())
  544. url = reverse("hc-details", args=[copied.code])
  545. return redirect(url + "?copied")
  546. @login_required
  547. def status_single(request, code):
  548. check, rw = _get_check_for_user(request, code)
  549. status = check.get_status()
  550. events = _get_events(check, 20)
  551. updated = "1"
  552. if len(events):
  553. updated = str(events[0].created.timestamp())
  554. doc = {
  555. "status": status,
  556. "status_text": STATUS_TEXT_TMPL.render({"check": check, "rw": rw}),
  557. "title": down_title(check),
  558. "updated": updated,
  559. }
  560. if updated != request.GET.get("u"):
  561. doc["events"] = EVENTS_TMPL.render({"check": check, "events": events})
  562. doc["downtimes"] = DOWNTIMES_TMPL.render({"downtimes": check.downtimes(3)})
  563. return JsonResponse(doc)
  564. @login_required
  565. def badges(request, code):
  566. project, rw = _get_project_for_user(request, code)
  567. tags = set()
  568. for check in Check.objects.filter(project=project):
  569. tags.update(check.tags_list())
  570. sorted_tags = sorted(tags, key=lambda s: s.lower())
  571. sorted_tags.append("*") # For the "overall status" badge
  572. key = project.badge_key
  573. urls = []
  574. for tag in sorted_tags:
  575. urls.append(
  576. {
  577. "tag": tag,
  578. "svg": get_badge_url(key, tag),
  579. "svg3": get_badge_url(key, tag, with_late=True),
  580. "json": get_badge_url(key, tag, fmt="json"),
  581. "json3": get_badge_url(key, tag, fmt="json", with_late=True),
  582. "shields": get_badge_url(key, tag, fmt="shields"),
  583. "shields3": get_badge_url(key, tag, fmt="shields", with_late=True),
  584. }
  585. )
  586. ctx = {
  587. "have_tags": len(urls) > 1,
  588. "page": "badges",
  589. "project": project,
  590. "badges": urls,
  591. }
  592. return render(request, "front/badges.html", ctx)
  593. @login_required
  594. def channels(request, code):
  595. project, rw = _get_project_for_user(request, code)
  596. if request.method == "POST":
  597. if not rw:
  598. return HttpResponseForbidden()
  599. code = request.POST["channel"]
  600. try:
  601. channel = Channel.objects.get(code=code)
  602. except Channel.DoesNotExist:
  603. return HttpResponseBadRequest()
  604. if channel.project_id != project.id:
  605. return HttpResponseForbidden()
  606. new_checks = []
  607. for key in request.POST:
  608. if key.startswith("check-"):
  609. code = key[6:]
  610. try:
  611. check = Check.objects.get(code=code)
  612. except Check.DoesNotExist:
  613. return HttpResponseBadRequest()
  614. if check.project_id != project.id:
  615. return HttpResponseForbidden()
  616. new_checks.append(check)
  617. channel.checks.set(new_checks)
  618. return redirect("hc-channels", project.code)
  619. channels = Channel.objects.filter(project=project)
  620. channels = channels.order_by("created")
  621. channels = channels.annotate(n_checks=Count("checks"))
  622. ctx = {
  623. "page": "channels",
  624. "rw": rw,
  625. "project": project,
  626. "profile": project.owner_profile,
  627. "channels": channels,
  628. "enable_apprise": settings.APPRISE_ENABLED is True,
  629. "enable_call": settings.TWILIO_AUTH is not None,
  630. "enable_discord": settings.DISCORD_CLIENT_ID is not None,
  631. "enable_linenotify": settings.LINENOTIFY_CLIENT_ID is not None,
  632. "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
  633. "enable_mattermost": settings.MATTERMOST_ENABLED is True,
  634. "enable_msteams": settings.MSTEAMS_ENABLED is True,
  635. "enable_opsgenie": settings.OPSGENIE_ENABLED is True,
  636. "enable_pagertree": settings.PAGERTREE_ENABLED is True,
  637. "enable_pd": settings.PD_ENABLED is True,
  638. "enable_pdc": settings.PD_VENDOR_KEY is not None,
  639. "enable_prometheus": settings.PROMETHEUS_ENABLED is True,
  640. "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
  641. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
  642. "enable_shell": settings.SHELL_ENABLED is True,
  643. "enable_signal": settings.SIGNAL_CLI_ENABLED is True,
  644. "enable_slack": settings.SLACK_ENABLED is True,
  645. "enable_slack_btn": settings.SLACK_CLIENT_ID is not None,
  646. "enable_sms": settings.TWILIO_AUTH is not None,
  647. "enable_spike": settings.SPIKE_ENABLED is True,
  648. "enable_telegram": settings.TELEGRAM_TOKEN is not None,
  649. "enable_trello": settings.TRELLO_APP_KEY is not None,
  650. "enable_victorops": settings.VICTOROPS_ENABLED is True,
  651. "enable_webhooks": settings.WEBHOOKS_ENABLED is True,
  652. "enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
  653. "enable_zulip": settings.ZULIP_ENABLED is True,
  654. "use_payments": settings.USE_PAYMENTS,
  655. }
  656. return render(request, "front/channels.html", ctx)
  657. @login_required
  658. def channel_checks(request, code):
  659. channel = _get_rw_channel_for_user(request, code)
  660. assigned = set(channel.checks.values_list("code", flat=True).distinct())
  661. checks = Check.objects.filter(project=channel.project).order_by("created")
  662. ctx = {"checks": checks, "assigned": assigned, "channel": channel}
  663. return render(request, "front/channel_checks.html", ctx)
  664. @require_POST
  665. @login_required
  666. def update_channel_name(request, code):
  667. channel = _get_rw_channel_for_user(request, code)
  668. form = forms.ChannelNameForm(request.POST)
  669. if form.is_valid():
  670. channel.name = form.cleaned_data["name"]
  671. channel.save()
  672. return redirect("hc-channels", channel.project.code)
  673. def verify_email(request, code, token):
  674. channel = get_object_or_404(Channel, code=code)
  675. if channel.make_token() == token:
  676. channel.email_verified = True
  677. channel.save()
  678. return render(request, "front/verify_email_success.html")
  679. return render(request, "bad_link.html")
  680. @csrf_exempt
  681. def unsubscribe_email(request, code, signed_token):
  682. # Some email servers open links in emails to check for malicious content.
  683. # To work around this, on GET requests we serve a confirmation form.
  684. # If the signature is at least 5 minutes old, we also include JS code to
  685. # auto-submit the form.
  686. ctx = {}
  687. if ":" in signed_token:
  688. signer = signing.TimestampSigner(salt="alerts")
  689. # First, check the signature without looking at the timestamp:
  690. try:
  691. token = signer.unsign(signed_token)
  692. except signing.BadSignature:
  693. return render(request, "bad_link.html")
  694. # Check if timestamp is older than 5 minutes:
  695. try:
  696. signer.unsign(signed_token, max_age=300)
  697. except signing.SignatureExpired:
  698. ctx["autosubmit"] = True
  699. else:
  700. token = signed_token
  701. channel = get_object_or_404(Channel, code=code, kind="email")
  702. if channel.make_token() != token:
  703. return render(request, "bad_link.html")
  704. if request.method != "POST":
  705. return render(request, "accounts/unsubscribe_submit.html", ctx)
  706. channel.delete()
  707. return render(request, "front/unsubscribe_success.html")
  708. @require_POST
  709. @login_required
  710. def send_test_notification(request, code):
  711. channel, rw = _get_channel_for_user(request, code)
  712. dummy = Check(name="TEST", status="down", project=channel.project)
  713. dummy.last_ping = timezone.now() - td(days=1)
  714. dummy.n_pings = 42
  715. if channel.kind == "webhook" and not channel.url_down:
  716. if channel.url_up:
  717. # If we don't have url_down, but do have have url_up then
  718. # send "TEST is UP" notification instead:
  719. dummy.status = "up"
  720. # Delete all older test notifications for this channel
  721. Notification.objects.filter(channel=channel, owner=None).delete()
  722. # Send the test notification
  723. error = channel.notify(dummy, is_test=True)
  724. if error:
  725. messages.warning(request, "Could not send a test notification. %s" % error)
  726. else:
  727. messages.success(request, "Test notification sent!")
  728. return redirect("hc-channels", channel.project.code)
  729. @require_POST
  730. @login_required
  731. def remove_channel(request, code):
  732. channel = _get_rw_channel_for_user(request, code)
  733. project = channel.project
  734. channel.delete()
  735. return redirect("hc-channels", project.code)
  736. @login_required
  737. def add_email(request, code):
  738. project = _get_rw_project_for_user(request, code)
  739. if request.method == "POST":
  740. form = forms.AddEmailForm(request.POST)
  741. if form.is_valid():
  742. channel = Channel(project=project, kind="email")
  743. channel.value = json.dumps(
  744. {
  745. "value": form.cleaned_data["value"],
  746. "up": form.cleaned_data["up"],
  747. "down": form.cleaned_data["down"],
  748. }
  749. )
  750. channel.save()
  751. channel.assign_all_checks()
  752. is_own_email = form.cleaned_data["value"] == request.user.email
  753. if is_own_email or not settings.EMAIL_USE_VERIFICATION:
  754. # If user is subscribing *their own* address
  755. # we can skip the verification step.
  756. # Additionally, in self-hosted setting, administator has the
  757. # option to disable the email verification step altogether.
  758. channel.email_verified = True
  759. channel.save()
  760. else:
  761. channel.send_verify_link()
  762. return redirect("hc-channels", project.code)
  763. else:
  764. form = forms.AddEmailForm()
  765. ctx = {
  766. "page": "channels",
  767. "project": project,
  768. "use_verification": settings.EMAIL_USE_VERIFICATION,
  769. "form": form,
  770. }
  771. return render(request, "integrations/add_email.html", ctx)
  772. @require_setting("WEBHOOKS_ENABLED")
  773. @login_required
  774. def add_webhook(request, code):
  775. project = _get_rw_project_for_user(request, code)
  776. if request.method == "POST":
  777. form = forms.WebhookForm(request.POST)
  778. if form.is_valid():
  779. channel = Channel(project=project, kind="webhook")
  780. channel.name = form.cleaned_data["name"]
  781. channel.value = form.get_value()
  782. channel.save()
  783. channel.assign_all_checks()
  784. return redirect("hc-channels", project.code)
  785. else:
  786. form = forms.WebhookForm()
  787. ctx = {
  788. "page": "channels",
  789. "project": project,
  790. "form": form,
  791. }
  792. return render(request, "integrations/webhook_form.html", ctx)
  793. @login_required
  794. def edit_webhook(request, code):
  795. channel = _get_rw_channel_for_user(request, code)
  796. if channel.kind != "webhook":
  797. return HttpResponseBadRequest()
  798. if request.method == "POST":
  799. form = forms.WebhookForm(request.POST)
  800. if form.is_valid():
  801. channel.name = form.cleaned_data["name"]
  802. channel.value = form.get_value()
  803. channel.save()
  804. return redirect("hc-channels", channel.project.code)
  805. else:
  806. def flatten(d):
  807. return "\n".join("%s: %s" % pair for pair in d.items())
  808. doc = json.loads(channel.value)
  809. doc["headers_down"] = flatten(doc["headers_down"])
  810. doc["headers_up"] = flatten(doc["headers_up"])
  811. doc["name"] = channel.name
  812. form = forms.WebhookForm(doc)
  813. ctx = {
  814. "page": "channels",
  815. "project": channel.project,
  816. "channel": channel,
  817. "form": form,
  818. }
  819. return render(request, "integrations/webhook_form.html", ctx)
  820. @require_setting("SHELL_ENABLED")
  821. @login_required
  822. def add_shell(request, code):
  823. project = _get_rw_project_for_user(request, code)
  824. if request.method == "POST":
  825. form = forms.AddShellForm(request.POST)
  826. if form.is_valid():
  827. channel = Channel(project=project, kind="shell")
  828. channel.value = form.get_value()
  829. channel.save()
  830. channel.assign_all_checks()
  831. return redirect("hc-channels", project.code)
  832. else:
  833. form = forms.AddShellForm()
  834. ctx = {
  835. "page": "channels",
  836. "project": project,
  837. "form": form,
  838. }
  839. return render(request, "integrations/add_shell.html", ctx)
  840. @require_setting("PD_ENABLED")
  841. @login_required
  842. def add_pd(request, code):
  843. project = _get_rw_project_for_user(request, code)
  844. # Simple Install Flow
  845. if settings.PD_APP_ID:
  846. state = token_urlsafe()
  847. redirect_url = settings.SITE_ROOT + reverse("hc-add-pd-complete")
  848. redirect_url += "?" + urlencode({"state": state})
  849. install_url = "https://app.pagerduty.com/install/integration?" + urlencode(
  850. {"app_id": settings.PD_APP_ID, "redirect_url": redirect_url, "version": "2"}
  851. )
  852. ctx = {"page": "channels", "project": project, "install_url": install_url}
  853. request.session["pagerduty"] = (state, str(project.code))
  854. return render(request, "integrations/add_pd_simple.html", ctx)
  855. if request.method == "POST":
  856. form = forms.AddPdForm(request.POST)
  857. if form.is_valid():
  858. channel = Channel(project=project, kind="pd")
  859. channel.value = form.cleaned_data["value"]
  860. channel.save()
  861. channel.assign_all_checks()
  862. return redirect("hc-channels", project.code)
  863. else:
  864. form = forms.AddPdForm()
  865. ctx = {"page": "channels", "project": project, "form": form}
  866. return render(request, "integrations/add_pd.html", ctx)
  867. @require_setting("PD_ENABLED")
  868. @require_setting("PD_APP_ID")
  869. @login_required
  870. def add_pd_complete(request):
  871. if "pagerduty" not in request.session:
  872. return HttpResponseBadRequest()
  873. state, code = request.session.pop("pagerduty")
  874. if request.GET.get("state") != state:
  875. return HttpResponseForbidden()
  876. project = _get_rw_project_for_user(request, code)
  877. doc = json.loads(request.GET["config"])
  878. for item in doc["integration_keys"]:
  879. channel = Channel(kind="pd", project=project)
  880. channel.name = item["name"]
  881. channel.value = json.dumps(
  882. {"service_key": item["integration_key"], "account": doc["account"]["name"]}
  883. )
  884. channel.save()
  885. channel.assign_all_checks()
  886. messages.success(request, "The PagerDuty integration has been added!")
  887. return redirect("hc-channels", project.code)
  888. @require_setting("PD_ENABLED")
  889. @require_setting("PD_VENDOR_KEY")
  890. def pdc_help(request):
  891. ctx = {"page": "channels"}
  892. return render(request, "integrations/add_pdc.html", ctx)
  893. @require_setting("PD_ENABLED")
  894. @require_setting("PD_VENDOR_KEY")
  895. @login_required
  896. def add_pdc(request, code):
  897. project = _get_rw_project_for_user(request, code)
  898. state = token_urlsafe()
  899. callback = settings.SITE_ROOT + reverse(
  900. "hc-add-pdc-complete", args=[project.code, state]
  901. )
  902. connect_url = "https://connect.pagerduty.com/connect?" + urlencode(
  903. {"vendor": settings.PD_VENDOR_KEY, "callback": callback}
  904. )
  905. ctx = {"page": "channels", "project": project, "connect_url": connect_url}
  906. request.session["pd"] = state
  907. return render(request, "integrations/add_pdc.html", ctx)
  908. @require_setting("PD_ENABLED")
  909. @require_setting("PD_VENDOR_KEY")
  910. @login_required
  911. def add_pdc_complete(request, code, state):
  912. if "pd" not in request.session:
  913. return HttpResponseBadRequest()
  914. project = _get_rw_project_for_user(request, code)
  915. session_state = request.session.pop("pd")
  916. if session_state != state:
  917. return HttpResponseBadRequest()
  918. if request.GET.get("error") == "cancelled":
  919. messages.warning(request, "PagerDuty setup was cancelled.")
  920. return redirect("hc-channels", project.code)
  921. channel = Channel(kind="pd", project=project)
  922. channel.value = json.dumps(
  923. {
  924. "service_key": request.GET.get("service_key"),
  925. "account": request.GET.get("account"),
  926. }
  927. )
  928. channel.save()
  929. channel.assign_all_checks()
  930. messages.success(request, "The PagerDuty integration has been added!")
  931. return redirect("hc-channels", project.code)
  932. @require_setting("PAGERTREE_ENABLED")
  933. @login_required
  934. def add_pagertree(request, code):
  935. project = _get_rw_project_for_user(request, code)
  936. if request.method == "POST":
  937. form = forms.AddUrlForm(request.POST)
  938. if form.is_valid():
  939. channel = Channel(project=project, kind="pagertree")
  940. channel.value = form.cleaned_data["value"]
  941. channel.save()
  942. channel.assign_all_checks()
  943. return redirect("hc-channels", project.code)
  944. else:
  945. form = forms.AddUrlForm()
  946. ctx = {"page": "channels", "project": project, "form": form}
  947. return render(request, "integrations/add_pagertree.html", ctx)
  948. @require_setting("SLACK_ENABLED")
  949. @login_required
  950. def add_slack(request, code):
  951. project = _get_rw_project_for_user(request, code)
  952. if request.method == "POST":
  953. form = forms.AddUrlForm(request.POST)
  954. if form.is_valid():
  955. channel = Channel(project=project, kind="slack")
  956. channel.value = form.cleaned_data["value"]
  957. channel.save()
  958. channel.assign_all_checks()
  959. return redirect("hc-channels", project.code)
  960. else:
  961. form = forms.AddUrlForm()
  962. ctx = {
  963. "page": "channels",
  964. "form": form,
  965. }
  966. return render(request, "integrations/add_slack.html", ctx)
  967. @require_setting("SLACK_ENABLED")
  968. @require_setting("SLACK_CLIENT_ID")
  969. def slack_help(request):
  970. ctx = {"page": "channels"}
  971. return render(request, "integrations/add_slack_btn.html", ctx)
  972. @require_setting("SLACK_ENABLED")
  973. @require_setting("SLACK_CLIENT_ID")
  974. @login_required
  975. def add_slack_btn(request, code):
  976. project = _get_rw_project_for_user(request, code)
  977. state = token_urlsafe()
  978. authorize_url = "https://slack.com/oauth/v2/authorize?" + urlencode(
  979. {
  980. "scope": "incoming-webhook",
  981. "client_id": settings.SLACK_CLIENT_ID,
  982. "state": state,
  983. }
  984. )
  985. ctx = {
  986. "project": project,
  987. "page": "channels",
  988. "authorize_url": authorize_url,
  989. }
  990. request.session["add_slack"] = (state, str(project.code))
  991. return render(request, "integrations/add_slack_btn.html", ctx)
  992. @require_setting("SLACK_ENABLED")
  993. @require_setting("SLACK_CLIENT_ID")
  994. @login_required
  995. def add_slack_complete(request):
  996. if "add_slack" not in request.session:
  997. return HttpResponseForbidden()
  998. state, code = request.session.pop("add_slack")
  999. project = _get_rw_project_for_user(request, code)
  1000. if request.GET.get("error") == "access_denied":
  1001. messages.warning(request, "Slack setup was cancelled.")
  1002. return redirect("hc-channels", project.code)
  1003. if request.GET.get("state") != state:
  1004. return HttpResponseForbidden()
  1005. result = requests.post(
  1006. "https://slack.com/api/oauth.v2.access",
  1007. {
  1008. "client_id": settings.SLACK_CLIENT_ID,
  1009. "client_secret": settings.SLACK_CLIENT_SECRET,
  1010. "code": request.GET.get("code"),
  1011. },
  1012. )
  1013. doc = result.json()
  1014. if doc.get("ok"):
  1015. channel = Channel(kind="slack", project=project)
  1016. channel.value = result.text
  1017. channel.save()
  1018. channel.assign_all_checks()
  1019. messages.success(request, "The Slack integration has been added!")
  1020. else:
  1021. s = doc.get("error")
  1022. messages.warning(request, "Error message from slack: %s" % s)
  1023. return redirect("hc-channels", project.code)
  1024. @require_setting("MATTERMOST_ENABLED")
  1025. @login_required
  1026. def add_mattermost(request, code):
  1027. project = _get_rw_project_for_user(request, code)
  1028. if request.method == "POST":
  1029. form = forms.AddUrlForm(request.POST)
  1030. if form.is_valid():
  1031. channel = Channel(project=project, kind="mattermost")
  1032. channel.value = form.cleaned_data["value"]
  1033. channel.save()
  1034. channel.assign_all_checks()
  1035. return redirect("hc-channels", project.code)
  1036. else:
  1037. form = forms.AddUrlForm()
  1038. ctx = {"page": "channels", "form": form, "project": project}
  1039. return render(request, "integrations/add_mattermost.html", ctx)
  1040. @require_setting("PUSHBULLET_CLIENT_ID")
  1041. @login_required
  1042. def add_pushbullet(request, code):
  1043. project = _get_rw_project_for_user(request, code)
  1044. state = token_urlsafe()
  1045. authorize_url = "https://www.pushbullet.com/authorize?" + urlencode(
  1046. {
  1047. "client_id": settings.PUSHBULLET_CLIENT_ID,
  1048. "redirect_uri": settings.SITE_ROOT + reverse(add_pushbullet_complete),
  1049. "response_type": "code",
  1050. "state": state,
  1051. }
  1052. )
  1053. ctx = {
  1054. "page": "channels",
  1055. "project": project,
  1056. "authorize_url": authorize_url,
  1057. }
  1058. request.session["add_pushbullet"] = (state, str(project.code))
  1059. return render(request, "integrations/add_pushbullet.html", ctx)
  1060. @require_setting("PUSHBULLET_CLIENT_ID")
  1061. @login_required
  1062. def add_pushbullet_complete(request):
  1063. if "add_pushbullet" not in request.session:
  1064. return HttpResponseForbidden()
  1065. state, code = request.session.pop("add_pushbullet")
  1066. project = _get_rw_project_for_user(request, code)
  1067. if request.GET.get("error") == "access_denied":
  1068. messages.warning(request, "Pushbullet setup was cancelled.")
  1069. return redirect("hc-channels", project.code)
  1070. if request.GET.get("state") != state:
  1071. return HttpResponseForbidden()
  1072. result = requests.post(
  1073. "https://api.pushbullet.com/oauth2/token",
  1074. {
  1075. "client_id": settings.PUSHBULLET_CLIENT_ID,
  1076. "client_secret": settings.PUSHBULLET_CLIENT_SECRET,
  1077. "code": request.GET.get("code"),
  1078. "grant_type": "authorization_code",
  1079. },
  1080. )
  1081. doc = result.json()
  1082. if "access_token" in doc:
  1083. channel = Channel(kind="pushbullet", project=project)
  1084. channel.value = doc["access_token"]
  1085. channel.save()
  1086. channel.assign_all_checks()
  1087. messages.success(request, "The Pushbullet integration has been added!")
  1088. else:
  1089. messages.warning(request, "Something went wrong")
  1090. return redirect("hc-channels", project.code)
  1091. @require_setting("DISCORD_CLIENT_ID")
  1092. @login_required
  1093. def add_discord(request, code):
  1094. project = _get_rw_project_for_user(request, code)
  1095. state = token_urlsafe()
  1096. auth_url = "https://discordapp.com/api/oauth2/authorize?" + urlencode(
  1097. {
  1098. "client_id": settings.DISCORD_CLIENT_ID,
  1099. "scope": "webhook.incoming",
  1100. "redirect_uri": settings.SITE_ROOT + reverse(add_discord_complete),
  1101. "response_type": "code",
  1102. "state": state,
  1103. }
  1104. )
  1105. ctx = {"page": "channels", "project": project, "authorize_url": auth_url}
  1106. request.session["add_discord"] = (state, str(project.code))
  1107. return render(request, "integrations/add_discord.html", ctx)
  1108. @require_setting("DISCORD_CLIENT_ID")
  1109. @login_required
  1110. def add_discord_complete(request):
  1111. if "add_discord" not in request.session:
  1112. return HttpResponseForbidden()
  1113. state, code = request.session.pop("add_discord")
  1114. project = _get_rw_project_for_user(request, code)
  1115. if request.GET.get("error") == "access_denied":
  1116. messages.warning(request, "Discord setup was cancelled.")
  1117. return redirect("hc-channels", project.code)
  1118. if request.GET.get("state") != state:
  1119. return HttpResponseForbidden()
  1120. result = requests.post(
  1121. "https://discordapp.com/api/oauth2/token",
  1122. {
  1123. "client_id": settings.DISCORD_CLIENT_ID,
  1124. "client_secret": settings.DISCORD_CLIENT_SECRET,
  1125. "code": request.GET.get("code"),
  1126. "grant_type": "authorization_code",
  1127. "redirect_uri": settings.SITE_ROOT + reverse(add_discord_complete),
  1128. },
  1129. )
  1130. doc = result.json()
  1131. if "access_token" in doc:
  1132. channel = Channel(kind="discord", project=project)
  1133. channel.value = result.text
  1134. channel.save()
  1135. channel.assign_all_checks()
  1136. messages.success(request, "The Discord integration has been added!")
  1137. else:
  1138. messages.warning(request, "Something went wrong.")
  1139. return redirect("hc-channels", project.code)
  1140. @require_setting("PUSHOVER_API_TOKEN")
  1141. def pushover_help(request):
  1142. ctx = {"page": "channels"}
  1143. return render(request, "integrations/add_pushover_help.html", ctx)
  1144. @require_setting("PUSHOVER_API_TOKEN")
  1145. @login_required
  1146. def add_pushover(request, code):
  1147. project = _get_rw_project_for_user(request, code)
  1148. if request.method == "POST":
  1149. state = token_urlsafe()
  1150. failure_url = settings.SITE_ROOT + reverse("hc-channels", args=[project.code])
  1151. success_url = (
  1152. settings.SITE_ROOT
  1153. + reverse("hc-add-pushover", args=[project.code])
  1154. + "?"
  1155. + urlencode(
  1156. {
  1157. "state": state,
  1158. "prio": request.POST.get("po_priority", "0"),
  1159. "prio_up": request.POST.get("po_priority_up", "0"),
  1160. }
  1161. )
  1162. )
  1163. subscription_url = (
  1164. settings.PUSHOVER_SUBSCRIPTION_URL
  1165. + "?"
  1166. + urlencode({"success": success_url, "failure": failure_url})
  1167. )
  1168. request.session["pushover"] = state
  1169. return redirect(subscription_url)
  1170. # Handle successful subscriptions
  1171. if "pushover_user_key" in request.GET:
  1172. if "pushover" not in request.session:
  1173. return HttpResponseForbidden()
  1174. state = request.session.pop("pushover")
  1175. if request.GET.get("state") != state:
  1176. return HttpResponseForbidden()
  1177. if request.GET.get("pushover_unsubscribed") == "1":
  1178. # Unsubscription: delete all Pushover channels for this project
  1179. Channel.objects.filter(project=project, kind="po").delete()
  1180. return redirect("hc-channels", project.code)
  1181. form = forms.AddPushoverForm(request.GET)
  1182. if not form.is_valid():
  1183. return HttpResponseBadRequest()
  1184. channel = Channel(project=project, kind="po")
  1185. channel.value = form.get_value()
  1186. channel.save()
  1187. channel.assign_all_checks()
  1188. messages.success(request, "The Pushover integration has been added!")
  1189. return redirect("hc-channels", project.code)
  1190. # Show Integration Settings form
  1191. ctx = {
  1192. "page": "channels",
  1193. "project": project,
  1194. "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY),
  1195. "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION),
  1196. }
  1197. return render(request, "integrations/add_pushover.html", ctx)
  1198. @require_setting("OPSGENIE_ENABLED")
  1199. @login_required
  1200. def add_opsgenie(request, code):
  1201. project = _get_rw_project_for_user(request, code)
  1202. if request.method == "POST":
  1203. form = forms.AddOpsgenieForm(request.POST)
  1204. if form.is_valid():
  1205. channel = Channel(project=project, kind="opsgenie")
  1206. v = {"region": form.cleaned_data["region"], "key": form.cleaned_data["key"]}
  1207. channel.value = json.dumps(v)
  1208. channel.save()
  1209. channel.assign_all_checks()
  1210. return redirect("hc-channels", project.code)
  1211. else:
  1212. form = forms.AddOpsgenieForm()
  1213. ctx = {"page": "channels", "project": project, "form": form}
  1214. return render(request, "integrations/add_opsgenie.html", ctx)
  1215. @require_setting("VICTOROPS_ENABLED")
  1216. @login_required
  1217. def add_victorops(request, code):
  1218. project = _get_rw_project_for_user(request, code)
  1219. if request.method == "POST":
  1220. form = forms.AddUrlForm(request.POST)
  1221. if form.is_valid():
  1222. channel = Channel(project=project, kind="victorops")
  1223. channel.value = form.cleaned_data["value"]
  1224. channel.save()
  1225. channel.assign_all_checks()
  1226. return redirect("hc-channels", project.code)
  1227. else:
  1228. form = forms.AddUrlForm()
  1229. ctx = {"page": "channels", "project": project, "form": form}
  1230. return render(request, "integrations/add_victorops.html", ctx)
  1231. @require_setting("ZULIP_ENABLED")
  1232. @login_required
  1233. def add_zulip(request, code):
  1234. project = _get_rw_project_for_user(request, code)
  1235. if request.method == "POST":
  1236. form = forms.AddZulipForm(request.POST)
  1237. if form.is_valid():
  1238. channel = Channel(project=project, kind="zulip")
  1239. channel.value = form.get_value()
  1240. channel.save()
  1241. channel.assign_all_checks()
  1242. return redirect("hc-channels", project.code)
  1243. else:
  1244. form = forms.AddZulipForm()
  1245. ctx = {"page": "channels", "project": project, "form": form}
  1246. return render(request, "integrations/add_zulip.html", ctx)
  1247. @csrf_exempt
  1248. @require_POST
  1249. def telegram_bot(request):
  1250. try:
  1251. doc = json.loads(request.body.decode())
  1252. jsonschema.validate(doc, telegram_callback)
  1253. except ValueError:
  1254. return HttpResponseBadRequest()
  1255. except jsonschema.ValidationError:
  1256. # We don't recognize the message format, but don't want Telegram
  1257. # retrying this over and over again, so respond with 200 OK
  1258. return HttpResponse()
  1259. if "/start" not in doc["message"]["text"]:
  1260. return HttpResponse()
  1261. chat = doc["message"]["chat"]
  1262. name = max(chat.get("title", ""), chat.get("username", ""))
  1263. invite = render_to_string(
  1264. "integrations/telegram_invite.html",
  1265. {"qs": signing.dumps((chat["id"], chat["type"], name))},
  1266. )
  1267. Telegram.send(chat["id"], invite)
  1268. return HttpResponse()
  1269. @require_setting("TELEGRAM_TOKEN")
  1270. def telegram_help(request):
  1271. ctx = {
  1272. "page": "channels",
  1273. "bot_name": settings.TELEGRAM_BOT_NAME,
  1274. }
  1275. return render(request, "integrations/add_telegram.html", ctx)
  1276. @require_setting("TELEGRAM_TOKEN")
  1277. @login_required
  1278. def add_telegram(request):
  1279. chat_id, chat_type, chat_name = None, None, None
  1280. qs = request.META["QUERY_STRING"]
  1281. if qs:
  1282. try:
  1283. chat_id, chat_type, chat_name = signing.loads(qs, max_age=600)
  1284. except signing.BadSignature:
  1285. return render(request, "bad_link.html")
  1286. if request.method == "POST":
  1287. project = _get_rw_project_for_user(request, request.POST.get("project"))
  1288. channel = Channel(project=project, kind="telegram")
  1289. channel.value = json.dumps(
  1290. {"id": chat_id, "type": chat_type, "name": chat_name}
  1291. )
  1292. channel.save()
  1293. channel.assign_all_checks()
  1294. messages.success(request, "The Telegram integration has been added!")
  1295. return redirect("hc-channels", project.code)
  1296. ctx = {
  1297. "page": "channels",
  1298. "projects": request.profile.projects(),
  1299. "chat_id": chat_id,
  1300. "chat_type": chat_type,
  1301. "chat_name": chat_name,
  1302. "bot_name": settings.TELEGRAM_BOT_NAME,
  1303. }
  1304. return render(request, "integrations/add_telegram.html", ctx)
  1305. @require_setting("TWILIO_AUTH")
  1306. @login_required
  1307. def add_sms(request, code):
  1308. project = _get_rw_project_for_user(request, code)
  1309. if request.method == "POST":
  1310. form = forms.PhoneUpDownForm(request.POST)
  1311. if form.is_valid():
  1312. channel = Channel(project=project, kind="sms")
  1313. channel.name = form.cleaned_data["label"]
  1314. channel.value = form.get_json()
  1315. channel.save()
  1316. channel.assign_all_checks()
  1317. return redirect("hc-channels", project.code)
  1318. else:
  1319. form = forms.PhoneUpDownForm(initial={"up": False})
  1320. ctx = {
  1321. "page": "channels",
  1322. "project": project,
  1323. "form": form,
  1324. "profile": project.owner_profile,
  1325. }
  1326. return render(request, "integrations/add_sms.html", ctx)
  1327. @require_setting("TWILIO_AUTH")
  1328. @login_required
  1329. def add_call(request, code):
  1330. project = _get_rw_project_for_user(request, code)
  1331. if request.method == "POST":
  1332. form = forms.PhoneNumberForm(request.POST)
  1333. if form.is_valid():
  1334. channel = Channel(project=project, kind="call")
  1335. channel.name = form.cleaned_data["label"]
  1336. channel.value = form.get_json()
  1337. channel.save()
  1338. channel.assign_all_checks()
  1339. return redirect("hc-channels", project.code)
  1340. else:
  1341. form = forms.PhoneNumberForm()
  1342. ctx = {
  1343. "page": "channels",
  1344. "project": project,
  1345. "form": form,
  1346. "profile": project.owner_profile,
  1347. }
  1348. return render(request, "integrations/add_call.html", ctx)
  1349. @require_setting("TWILIO_USE_WHATSAPP")
  1350. @login_required
  1351. def add_whatsapp(request, code):
  1352. project = _get_rw_project_for_user(request, code)
  1353. if request.method == "POST":
  1354. form = forms.PhoneUpDownForm(request.POST)
  1355. if form.is_valid():
  1356. channel = Channel(project=project, kind="whatsapp")
  1357. channel.name = form.cleaned_data["label"]
  1358. channel.value = form.get_json()
  1359. channel.save()
  1360. channel.assign_all_checks()
  1361. return redirect("hc-channels", project.code)
  1362. else:
  1363. form = forms.PhoneUpDownForm()
  1364. ctx = {
  1365. "page": "channels",
  1366. "project": project,
  1367. "form": form,
  1368. "profile": project.owner_profile,
  1369. }
  1370. return render(request, "integrations/add_whatsapp.html", ctx)
  1371. @require_setting("SIGNAL_CLI_ENABLED")
  1372. @login_required
  1373. def add_signal(request, code):
  1374. project = _get_rw_project_for_user(request, code)
  1375. if request.method == "POST":
  1376. form = forms.PhoneUpDownForm(request.POST)
  1377. if form.is_valid():
  1378. channel = Channel(project=project, kind="signal")
  1379. channel.name = form.cleaned_data["label"]
  1380. channel.value = form.get_json()
  1381. channel.save()
  1382. channel.assign_all_checks()
  1383. return redirect("hc-channels", project.code)
  1384. else:
  1385. form = forms.PhoneUpDownForm()
  1386. ctx = {
  1387. "page": "channels",
  1388. "project": project,
  1389. "form": form,
  1390. "profile": project.owner_profile,
  1391. }
  1392. return render(request, "integrations/add_signal.html", ctx)
  1393. @require_setting("TRELLO_APP_KEY")
  1394. @login_required
  1395. def add_trello(request, code):
  1396. project = _get_rw_project_for_user(request, code)
  1397. if request.method == "POST":
  1398. form = forms.AddTrelloForm(request.POST)
  1399. if not form.is_valid():
  1400. return HttpResponseBadRequest()
  1401. channel = Channel(project=project, kind="trello")
  1402. channel.value = form.get_value()
  1403. channel.save()
  1404. channel.assign_all_checks()
  1405. return redirect("hc-channels", project.code)
  1406. return_url = settings.SITE_ROOT + reverse("hc-add-trello", args=[project.code])
  1407. authorize_url = "https://trello.com/1/authorize?" + urlencode(
  1408. {
  1409. "expiration": "never",
  1410. "name": settings.SITE_NAME,
  1411. "scope": "read,write",
  1412. "response_type": "token",
  1413. "key": settings.TRELLO_APP_KEY,
  1414. "return_url": return_url,
  1415. }
  1416. )
  1417. ctx = {
  1418. "page": "channels",
  1419. "project": project,
  1420. "authorize_url": authorize_url,
  1421. }
  1422. return render(request, "integrations/add_trello.html", ctx)
  1423. @require_setting("MATRIX_ACCESS_TOKEN")
  1424. @login_required
  1425. def add_matrix(request, code):
  1426. project = _get_rw_project_for_user(request, code)
  1427. if request.method == "POST":
  1428. form = forms.AddMatrixForm(request.POST)
  1429. if form.is_valid():
  1430. channel = Channel(project=project, kind="matrix")
  1431. channel.value = form.cleaned_data["room_id"]
  1432. # If user supplied room alias instead of ID, use it as channel name
  1433. alias = form.cleaned_data["alias"]
  1434. if not alias.startswith("!"):
  1435. channel.name = alias
  1436. channel.save()
  1437. channel.assign_all_checks()
  1438. messages.success(request, "The Matrix integration has been added!")
  1439. return redirect("hc-channels", project.code)
  1440. else:
  1441. form = forms.AddMatrixForm()
  1442. ctx = {
  1443. "page": "channels",
  1444. "project": project,
  1445. "form": form,
  1446. "matrix_user_id": settings.MATRIX_USER_ID,
  1447. }
  1448. return render(request, "integrations/add_matrix.html", ctx)
  1449. @require_setting("APPRISE_ENABLED")
  1450. @login_required
  1451. def add_apprise(request, code):
  1452. project = _get_rw_project_for_user(request, code)
  1453. if request.method == "POST":
  1454. form = forms.AddAppriseForm(request.POST)
  1455. if form.is_valid():
  1456. channel = Channel(project=project, kind="apprise")
  1457. channel.value = form.cleaned_data["url"]
  1458. channel.save()
  1459. channel.assign_all_checks()
  1460. messages.success(request, "The Apprise integration has been added!")
  1461. return redirect("hc-channels", project.code)
  1462. else:
  1463. form = forms.AddAppriseForm()
  1464. ctx = {"page": "channels", "project": project, "form": form}
  1465. return render(request, "integrations/add_apprise.html", ctx)
  1466. @require_setting("TRELLO_APP_KEY")
  1467. @login_required
  1468. @require_POST
  1469. def trello_settings(request):
  1470. token = request.POST.get("token")
  1471. url = "https://api.trello.com/1/members/me/boards?" + urlencode(
  1472. {
  1473. "key": settings.TRELLO_APP_KEY,
  1474. "token": token,
  1475. "filter": "open",
  1476. "fields": "id,name",
  1477. "lists": "open",
  1478. "list_fields": "id,name",
  1479. }
  1480. )
  1481. boards = requests.get(url).json()
  1482. num_lists = sum(len(board["lists"]) for board in boards)
  1483. ctx = {"token": token, "boards": boards, "num_lists": num_lists}
  1484. return render(request, "integrations/trello_settings.html", ctx)
  1485. @require_setting("MSTEAMS_ENABLED")
  1486. @login_required
  1487. def add_msteams(request, code):
  1488. project = _get_rw_project_for_user(request, code)
  1489. if request.method == "POST":
  1490. form = forms.AddUrlForm(request.POST)
  1491. if form.is_valid():
  1492. channel = Channel(project=project, kind="msteams")
  1493. channel.value = form.cleaned_data["value"]
  1494. channel.save()
  1495. channel.assign_all_checks()
  1496. return redirect("hc-channels", project.code)
  1497. else:
  1498. form = forms.AddUrlForm()
  1499. ctx = {"page": "channels", "project": project, "form": form}
  1500. return render(request, "integrations/add_msteams.html", ctx)
  1501. @require_setting("PROMETHEUS_ENABLED")
  1502. @login_required
  1503. def add_prometheus(request, code):
  1504. project, rw = _get_project_for_user(request, code)
  1505. ctx = {"page": "channels", "project": project}
  1506. return render(request, "integrations/add_prometheus.html", ctx)
  1507. @require_setting("PROMETHEUS_ENABLED")
  1508. def metrics(request, code, key):
  1509. if len(key) != 32:
  1510. return HttpResponseBadRequest()
  1511. q = Project.objects.filter(code=code, api_key_readonly=key)
  1512. try:
  1513. project = q.get()
  1514. except Project.DoesNotExist:
  1515. return HttpResponseForbidden()
  1516. checks = Check.objects.filter(project_id=project.id).order_by("id")
  1517. def esc(s):
  1518. return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
  1519. def output(checks):
  1520. yield "# HELP hc_check_up Whether the check is currently up (1 for yes, 0 for no).\n"
  1521. yield "# TYPE hc_check_up gauge\n"
  1522. TMPL = """hc_check_up{name="%s", tags="%s", unique_key="%s"} %d\n"""
  1523. for check in checks:
  1524. value = 0 if check.get_status() == "down" else 1
  1525. yield TMPL % (esc(check.name), esc(check.tags), check.unique_key, value)
  1526. tags_statuses, num_down = _tags_statuses(checks)
  1527. yield "\n"
  1528. yield "# HELP hc_tag_up Whether all checks with this tag are up (1 for yes, 0 for no).\n"
  1529. yield "# TYPE hc_tag_up gauge\n"
  1530. TMPL = """hc_tag_up{tag="%s"} %d\n"""
  1531. for tag in sorted(tags_statuses):
  1532. value = 0 if tags_statuses[tag] == "down" else 1
  1533. yield TMPL % (esc(tag), value)
  1534. yield "\n"
  1535. yield "# HELP hc_checks_total The total number of checks.\n"
  1536. yield "# TYPE hc_checks_total gauge\n"
  1537. yield "hc_checks_total %d\n" % len(checks)
  1538. yield "\n"
  1539. yield "# HELP hc_checks_down_total The number of checks currently down.\n"
  1540. yield "# TYPE hc_checks_down_total gauge\n"
  1541. yield "hc_checks_down_total %d\n" % num_down
  1542. return HttpResponse(output(checks), content_type="text/plain")
  1543. @require_setting("SPIKE_ENABLED")
  1544. @login_required
  1545. def add_spike(request, code):
  1546. project = _get_rw_project_for_user(request, code)
  1547. if request.method == "POST":
  1548. form = forms.AddUrlForm(request.POST)
  1549. if form.is_valid():
  1550. channel = Channel(project=project, kind="spike")
  1551. channel.value = form.cleaned_data["value"]
  1552. channel.save()
  1553. channel.assign_all_checks()
  1554. return redirect("hc-channels", project.code)
  1555. else:
  1556. form = forms.AddUrlForm()
  1557. ctx = {"page": "channels", "project": project, "form": form}
  1558. return render(request, "integrations/add_spike.html", ctx)
  1559. @require_setting("LINENOTIFY_CLIENT_ID")
  1560. @login_required
  1561. def add_linenotify(request, code):
  1562. project = _get_rw_project_for_user(request, code)
  1563. state = token_urlsafe()
  1564. authorize_url = " https://notify-bot.line.me/oauth/authorize?" + urlencode(
  1565. {
  1566. "client_id": settings.LINENOTIFY_CLIENT_ID,
  1567. "redirect_uri": settings.SITE_ROOT + reverse(add_linenotify_complete),
  1568. "response_type": "code",
  1569. "state": state,
  1570. "scope": "notify",
  1571. }
  1572. )
  1573. ctx = {
  1574. "page": "channels",
  1575. "project": project,
  1576. "authorize_url": authorize_url,
  1577. }
  1578. request.session["add_linenotify"] = (state, str(project.code))
  1579. return render(request, "integrations/add_linenotify.html", ctx)
  1580. @require_setting("LINENOTIFY_CLIENT_ID")
  1581. @login_required
  1582. def add_linenotify_complete(request):
  1583. if "add_linenotify" not in request.session:
  1584. return HttpResponseForbidden()
  1585. state, code = request.session.pop("add_linenotify")
  1586. if request.GET.get("state") != state:
  1587. return HttpResponseForbidden()
  1588. project = _get_rw_project_for_user(request, code)
  1589. if request.GET.get("error") == "access_denied":
  1590. messages.warning(request, "LINE Notify setup was cancelled.")
  1591. return redirect("hc-channels", project.code)
  1592. # Exchange code for access token
  1593. result = requests.post(
  1594. "https://notify-bot.line.me/oauth/token",
  1595. {
  1596. "grant_type": "authorization_code",
  1597. "code": request.GET.get("code"),
  1598. "redirect_uri": settings.SITE_ROOT + reverse(add_linenotify_complete),
  1599. "client_id": settings.LINENOTIFY_CLIENT_ID,
  1600. "client_secret": settings.LINENOTIFY_CLIENT_SECRET,
  1601. },
  1602. )
  1603. doc = result.json()
  1604. if doc.get("status") != 200:
  1605. messages.warning(request, "Something went wrong.")
  1606. return redirect("hc-channels", project.code)
  1607. # Fetch notification target's name, will use it as channel name:
  1608. token = doc["access_token"]
  1609. result = requests.get(
  1610. "https://notify-api.line.me/api/status",
  1611. headers={"Authorization": "Bearer %s" % token},
  1612. )
  1613. doc = result.json()
  1614. channel = Channel(kind="linenotify", project=project)
  1615. channel.name = doc.get("target")
  1616. channel.value = token
  1617. channel.save()
  1618. channel.assign_all_checks()
  1619. messages.success(request, "The LINE Notify integration has been added!")
  1620. return redirect("hc-channels", project.code)
  1621. # Forks: add custom views after this line