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.

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