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.

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