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.

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