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.

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