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.

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