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.

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