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.

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