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.

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