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.

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