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.

1394 lines
41 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
9 years ago
9 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
9 years ago
9 years ago
6 years ago
6 years ago
8 years ago
8 years ago
  1. from datetime import datetime, timedelta as td
  2. import json
  3. from urllib.parse import urlencode
  4. from croniter import croniter
  5. from django.conf import settings
  6. from django.contrib import messages
  7. from django.contrib.auth.decorators import login_required
  8. from django.core import signing
  9. from django.db.models import Count
  10. from django.http import (
  11. Http404,
  12. HttpResponse,
  13. HttpResponseBadRequest,
  14. HttpResponseForbidden,
  15. JsonResponse,
  16. )
  17. from django.shortcuts import get_object_or_404, redirect, render
  18. from django.template.loader import get_template, render_to_string
  19. from django.urls import reverse
  20. from django.utils import timezone
  21. from django.utils.crypto import get_random_string
  22. from django.views.decorators.csrf import csrf_exempt
  23. from django.views.decorators.http import require_POST
  24. from hc.accounts.models import Project
  25. from hc.api.models import (
  26. DEFAULT_GRACE,
  27. DEFAULT_TIMEOUT,
  28. MAX_DELTA,
  29. Channel,
  30. Check,
  31. Ping,
  32. Notification,
  33. )
  34. from hc.api.transports import Telegram
  35. from hc.front.forms import (
  36. AddWebhookForm,
  37. NameTagsForm,
  38. TimeoutForm,
  39. AddUrlForm,
  40. AddEmailForm,
  41. AddOpsGenieForm,
  42. CronForm,
  43. AddSmsForm,
  44. ChannelNameForm,
  45. EmailSettingsForm,
  46. AddMatrixForm,
  47. AddAppriseForm,
  48. )
  49. from hc.front.schemas import telegram_callback
  50. from hc.front.templatetags.hc_extras import num_down_title, down_title, sortchecks
  51. from hc.lib import jsonschema
  52. from hc.lib.badges import get_badge_url
  53. import pytz
  54. from pytz.exceptions import UnknownTimeZoneError
  55. import requests
  56. VALID_SORT_VALUES = ("name", "-name", "last_ping", "-last_ping", "created")
  57. STATUS_TEXT_TMPL = get_template("front/log_status_text.html")
  58. LAST_PING_TMPL = get_template("front/last_ping_cell.html")
  59. EVENTS_TMPL = get_template("front/details_events.html")
  60. DOWNTIMES_TMPL = get_template("front/details_downtimes.html")
  61. def _tags_statuses(checks):
  62. tags, down, grace, num_down = {}, {}, {}, 0
  63. for check in checks:
  64. status = check.get_status(with_started=False)
  65. if status == "down":
  66. num_down += 1
  67. for tag in check.tags_list():
  68. down[tag] = "down"
  69. elif status == "grace":
  70. for tag in check.tags_list():
  71. grace[tag] = "grace"
  72. else:
  73. for tag in check.tags_list():
  74. tags[tag] = "up"
  75. tags.update(grace)
  76. tags.update(down)
  77. return tags, num_down
  78. def _get_check_for_user(request, code):
  79. """ Return specified check if current user has access to it. """
  80. if not request.user.is_authenticated:
  81. raise Http404("not found")
  82. if request.user.is_superuser:
  83. q = Check.objects
  84. else:
  85. q = request.profile.checks_from_all_projects()
  86. try:
  87. return q.get(code=code)
  88. except Check.DoesNotExist:
  89. raise Http404("not found")
  90. def _get_project_for_user(request, project_code):
  91. """ Return true if current user has access to the specified account. """
  92. if request.user.is_superuser:
  93. q = Project.objects
  94. else:
  95. q = request.profile.projects()
  96. try:
  97. return q.get(code=project_code)
  98. except Project.DoesNotExist:
  99. raise Http404("not found")
  100. @login_required
  101. def my_checks(request, code):
  102. project = _get_project_for_user(request, code)
  103. if request.GET.get("sort") in VALID_SORT_VALUES:
  104. request.profile.sort = request.GET["sort"]
  105. request.profile.save()
  106. if request.profile.current_project_id != project.id:
  107. request.profile.current_project = project
  108. request.profile.save()
  109. q = Check.objects.filter(project=project)
  110. checks = list(q.prefetch_related("channel_set"))
  111. sortchecks(checks, request.profile.sort)
  112. tags_statuses, num_down = _tags_statuses(checks)
  113. pairs = list(tags_statuses.items())
  114. pairs.sort(key=lambda pair: pair[0].lower())
  115. channels = Channel.objects.filter(project=project)
  116. channels = list(channels.order_by("created"))
  117. hidden_checks = set()
  118. # Hide checks that don't match selected tags:
  119. selected_tags = set(request.GET.getlist("tag", []))
  120. if selected_tags:
  121. for check in checks:
  122. if not selected_tags.issubset(check.tags_list()):
  123. hidden_checks.add(check)
  124. # Hide checks that don't match the search string:
  125. search = request.GET.get("search", "")
  126. if search:
  127. for check in checks:
  128. search_key = "%s\n%s" % (check.name.lower(), check.code)
  129. if search not in search_key:
  130. hidden_checks.add(check)
  131. # Do we need to show the "Last Duration" header?
  132. show_last_duration = False
  133. for check in checks:
  134. if check.clamped_last_duration():
  135. show_last_duration = True
  136. break
  137. ctx = {
  138. "page": "checks",
  139. "checks": checks,
  140. "channels": channels,
  141. "num_down": num_down,
  142. "now": timezone.now(),
  143. "tags": pairs,
  144. "ping_endpoint": settings.PING_ENDPOINT,
  145. "timezones": pytz.all_timezones,
  146. "project": project,
  147. "num_available": project.num_checks_available(),
  148. "sort": request.profile.sort,
  149. "selected_tags": selected_tags,
  150. "search": search,
  151. "hidden_checks": hidden_checks,
  152. "show_last_duration": show_last_duration,
  153. }
  154. return render(request, "front/my_checks.html", ctx)
  155. @login_required
  156. def status(request, code):
  157. _get_project_for_user(request, code)
  158. checks = list(Check.objects.filter(project__code=code))
  159. details = []
  160. for check in checks:
  161. ctx = {"check": check}
  162. details.append(
  163. {
  164. "code": str(check.code),
  165. "status": check.get_status(),
  166. "last_ping": LAST_PING_TMPL.render(ctx),
  167. }
  168. )
  169. tags_statuses, num_down = _tags_statuses(checks)
  170. return JsonResponse(
  171. {"details": details, "tags": tags_statuses, "title": num_down_title(num_down)}
  172. )
  173. @login_required
  174. @require_POST
  175. def switch_channel(request, code, channel_code):
  176. check = _get_check_for_user(request, code)
  177. channel = get_object_or_404(Channel, code=channel_code)
  178. if channel.project_id != check.project_id:
  179. return HttpResponseBadRequest()
  180. if request.POST.get("state") == "on":
  181. channel.checks.add(check)
  182. else:
  183. channel.checks.remove(check)
  184. return HttpResponse()
  185. def index(request):
  186. if request.user.is_authenticated:
  187. projects = list(request.profile.projects())
  188. ctx = {"page": "projects", "projects": projects}
  189. return render(request, "front/projects.html", ctx)
  190. check = Check()
  191. ctx = {
  192. "page": "welcome",
  193. "check": check,
  194. "ping_url": check.url(),
  195. "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
  196. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
  197. "enable_discord": settings.DISCORD_CLIENT_ID is not None,
  198. "enable_telegram": settings.TELEGRAM_TOKEN is not None,
  199. "enable_sms": settings.TWILIO_AUTH is not None,
  200. "enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
  201. "enable_pd": settings.PD_VENDOR_KEY is not None,
  202. "enable_trello": settings.TRELLO_APP_KEY is not None,
  203. "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
  204. "enable_apprise": settings.APPRISE_ENABLED is True,
  205. "registration_open": settings.REGISTRATION_OPEN,
  206. }
  207. return render(request, "front/welcome.html", ctx)
  208. def docs(request):
  209. ctx = {
  210. "page": "docs",
  211. "section": "home",
  212. "ping_endpoint": settings.PING_ENDPOINT,
  213. "ping_email": "your-uuid-here@%s" % settings.PING_EMAIL_DOMAIN,
  214. "ping_email_domain": settings.PING_EMAIL_DOMAIN,
  215. "ping_url": settings.PING_ENDPOINT + "your-uuid-here",
  216. }
  217. return render(request, "front/docs.html", ctx)
  218. def docs_api(request):
  219. ctx = {
  220. "page": "docs",
  221. "section": "api",
  222. "SITE_ROOT": settings.SITE_ROOT,
  223. "PING_ENDPOINT": settings.PING_ENDPOINT,
  224. "default_timeout": int(DEFAULT_TIMEOUT.total_seconds()),
  225. "default_grace": int(DEFAULT_GRACE.total_seconds()),
  226. }
  227. return render(request, "front/docs_api.html", ctx)
  228. def docs_cron(request):
  229. ctx = {"page": "docs", "section": "cron"}
  230. return render(request, "front/docs_cron.html", ctx)
  231. def docs_resources(request):
  232. ctx = {"page": "docs", "section": "resources"}
  233. return render(request, "front/docs_resources.html", ctx)
  234. @require_POST
  235. @login_required
  236. def add_check(request, code):
  237. project = _get_project_for_user(request, code)
  238. if project.num_checks_available() <= 0:
  239. return HttpResponseBadRequest()
  240. check = Check(project=project)
  241. check.save()
  242. check.assign_all_channels()
  243. url = reverse("hc-details", args=[check.code])
  244. return redirect(url + "?new")
  245. @require_POST
  246. @login_required
  247. def update_name(request, code):
  248. check = _get_check_for_user(request, code)
  249. form = NameTagsForm(request.POST)
  250. if form.is_valid():
  251. check.name = form.cleaned_data["name"]
  252. check.tags = form.cleaned_data["tags"]
  253. check.desc = form.cleaned_data["desc"]
  254. check.save()
  255. if "/details/" in request.META.get("HTTP_REFERER", ""):
  256. return redirect("hc-details", code)
  257. return redirect("hc-checks", check.project.code)
  258. @require_POST
  259. @login_required
  260. def email_settings(request, code):
  261. check = _get_check_for_user(request, code)
  262. form = EmailSettingsForm(request.POST)
  263. if form.is_valid():
  264. check.subject = form.cleaned_data["subject"]
  265. check.save()
  266. return redirect("hc-details", code)
  267. @require_POST
  268. @login_required
  269. def update_timeout(request, code):
  270. check = _get_check_for_user(request, code)
  271. kind = request.POST.get("kind")
  272. if kind == "simple":
  273. form = TimeoutForm(request.POST)
  274. if not form.is_valid():
  275. return HttpResponseBadRequest()
  276. check.kind = "simple"
  277. check.timeout = form.cleaned_data["timeout"]
  278. check.grace = form.cleaned_data["grace"]
  279. elif kind == "cron":
  280. form = CronForm(request.POST)
  281. if not form.is_valid():
  282. return HttpResponseBadRequest()
  283. check.kind = "cron"
  284. check.schedule = form.cleaned_data["schedule"]
  285. check.tz = form.cleaned_data["tz"]
  286. check.grace = td(minutes=form.cleaned_data["grace"])
  287. check.alert_after = check.going_down_after()
  288. check.save()
  289. if "/details/" in request.META.get("HTTP_REFERER", ""):
  290. return redirect("hc-details", code)
  291. return redirect("hc-checks", check.project.code)
  292. @require_POST
  293. def cron_preview(request):
  294. schedule = request.POST.get("schedule", "")
  295. tz = request.POST.get("tz")
  296. ctx = {"tz": tz, "dates": []}
  297. try:
  298. zone = pytz.timezone(tz)
  299. now_local = timezone.localtime(timezone.now(), zone)
  300. if len(schedule.split()) != 5:
  301. raise ValueError()
  302. it = croniter(schedule, now_local)
  303. for i in range(0, 6):
  304. ctx["dates"].append(it.get_next(datetime))
  305. except UnknownTimeZoneError:
  306. ctx["bad_tz"] = True
  307. except:
  308. ctx["bad_schedule"] = True
  309. return render(request, "front/cron_preview.html", ctx)
  310. def ping_details(request, code, n=None):
  311. check = _get_check_for_user(request, code)
  312. q = Ping.objects.filter(owner=check)
  313. if n:
  314. q = q.filter(n=n)
  315. ping = q.latest("created")
  316. ctx = {"check": check, "ping": ping}
  317. return render(request, "front/ping_details.html", ctx)
  318. @require_POST
  319. @login_required
  320. def pause(request, code):
  321. check = _get_check_for_user(request, code)
  322. check.status = "paused"
  323. check.last_start = None
  324. check.alert_after = None
  325. check.save()
  326. if "/details/" in request.META.get("HTTP_REFERER", ""):
  327. return redirect("hc-details", code)
  328. return redirect("hc-checks", check.project.code)
  329. @require_POST
  330. @login_required
  331. def remove_check(request, code):
  332. check = _get_check_for_user(request, code)
  333. project = check.project
  334. check.delete()
  335. return redirect("hc-checks", project.code)
  336. def _get_events(check, limit):
  337. pings = Ping.objects.filter(owner=check).order_by("-id")[:limit]
  338. pings = list(pings)
  339. prev = None
  340. for ping in pings:
  341. if ping.kind == "start" and prev and prev.kind != "start":
  342. delta = prev.created - ping.created
  343. if delta < MAX_DELTA:
  344. setattr(prev, "delta", delta)
  345. prev = ping
  346. alerts = []
  347. if len(pings):
  348. cutoff = pings[-1].created
  349. alerts = Notification.objects.select_related("channel").filter(
  350. owner=check, check_status="down", created__gt=cutoff
  351. )
  352. events = pings + list(alerts)
  353. events.sort(key=lambda el: el.created, reverse=True)
  354. return events
  355. @login_required
  356. def log(request, code):
  357. check = _get_check_for_user(request, code)
  358. limit = check.project.owner_profile.ping_log_limit
  359. ctx = {
  360. "project": check.project,
  361. "check": check,
  362. "events": _get_events(check, limit),
  363. "limit": limit,
  364. "show_limit_notice": check.n_pings > limit and settings.USE_PAYMENTS,
  365. }
  366. return render(request, "front/log.html", ctx)
  367. @login_required
  368. def details(request, code):
  369. check = _get_check_for_user(request, code)
  370. channels = Channel.objects.filter(project=check.project)
  371. channels = list(channels.order_by("created"))
  372. ctx = {
  373. "page": "details",
  374. "project": check.project,
  375. "check": check,
  376. "channels": channels,
  377. "timezones": pytz.all_timezones,
  378. "downtimes": check.downtimes(months=3),
  379. "is_new": "new" in request.GET,
  380. }
  381. return render(request, "front/details.html", ctx)
  382. @login_required
  383. def transfer(request, code):
  384. check = _get_check_for_user(request, code)
  385. if request.method == "POST":
  386. target_project = _get_project_for_user(request, request.POST["project"])
  387. if target_project.num_checks_available() <= 0:
  388. return HttpResponseBadRequest()
  389. check.project = target_project
  390. check.save()
  391. check.assign_all_channels()
  392. request.profile.current_project = target_project
  393. request.profile.save()
  394. messages.success(request, "Check transferred successfully!")
  395. return redirect("hc-details", code)
  396. ctx = {"check": check}
  397. return render(request, "front/transfer_modal.html", ctx)
  398. @login_required
  399. def status_single(request, code):
  400. check = _get_check_for_user(request, code)
  401. status = check.get_status()
  402. events = _get_events(check, 20)
  403. updated = "1"
  404. if len(events):
  405. updated = str(events[0].created.timestamp())
  406. doc = {
  407. "status": status,
  408. "status_text": STATUS_TEXT_TMPL.render({"check": check}),
  409. "title": down_title(check),
  410. "updated": updated,
  411. }
  412. if updated != request.GET.get("u"):
  413. doc["events"] = EVENTS_TMPL.render({"check": check, "events": events})
  414. doc["downtimes"] = DOWNTIMES_TMPL.render({"downtimes": check.downtimes(3)})
  415. return JsonResponse(doc)
  416. @login_required
  417. def badges(request, code):
  418. project = _get_project_for_user(request, code)
  419. tags = set()
  420. for check in Check.objects.filter(project=project):
  421. tags.update(check.tags_list())
  422. sorted_tags = sorted(tags, key=lambda s: s.lower())
  423. sorted_tags.append("*") # For the "overall status" badge
  424. urls = []
  425. for tag in sorted_tags:
  426. urls.append(
  427. {
  428. "tag": tag,
  429. "svg": get_badge_url(project.badge_key, tag),
  430. "json": get_badge_url(project.badge_key, tag, format="json"),
  431. }
  432. )
  433. ctx = {
  434. "have_tags": len(urls) > 1,
  435. "page": "badges",
  436. "project": project,
  437. "badges": urls,
  438. }
  439. return render(request, "front/badges.html", ctx)
  440. @login_required
  441. def channels(request):
  442. if not request.project:
  443. # This can happen when the user deletes their only project.
  444. return redirect("hc-index")
  445. if request.method == "POST":
  446. code = request.POST["channel"]
  447. try:
  448. channel = Channel.objects.get(code=code)
  449. except Channel.DoesNotExist:
  450. return HttpResponseBadRequest()
  451. if channel.project_id != request.project.id:
  452. return HttpResponseForbidden()
  453. new_checks = []
  454. for key in request.POST:
  455. if key.startswith("check-"):
  456. code = key[6:]
  457. try:
  458. check = Check.objects.get(code=code)
  459. except Check.DoesNotExist:
  460. return HttpResponseBadRequest()
  461. if check.project_id != request.project.id:
  462. return HttpResponseForbidden()
  463. new_checks.append(check)
  464. channel.checks.set(new_checks)
  465. return redirect("hc-channels")
  466. channels = Channel.objects.filter(project=request.project)
  467. channels = channels.order_by("created")
  468. channels = channels.annotate(n_checks=Count("checks"))
  469. ctx = {
  470. "page": "channels",
  471. "project": request.project,
  472. "profile": request.project.owner_profile,
  473. "channels": channels,
  474. "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
  475. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
  476. "enable_discord": settings.DISCORD_CLIENT_ID is not None,
  477. "enable_telegram": settings.TELEGRAM_TOKEN is not None,
  478. "enable_sms": settings.TWILIO_AUTH is not None,
  479. "enable_whatsapp": settings.TWILIO_USE_WHATSAPP,
  480. "enable_pd": settings.PD_VENDOR_KEY is not None,
  481. "enable_trello": settings.TRELLO_APP_KEY is not None,
  482. "enable_matrix": settings.MATRIX_ACCESS_TOKEN is not None,
  483. "enable_apprise": settings.APPRISE_ENABLED is True,
  484. "use_payments": settings.USE_PAYMENTS,
  485. }
  486. return render(request, "front/channels.html", ctx)
  487. @login_required
  488. def channel_checks(request, code):
  489. channel = get_object_or_404(Channel, code=code)
  490. if channel.project_id != request.project.id:
  491. return HttpResponseForbidden()
  492. assigned = set(channel.checks.values_list("code", flat=True).distinct())
  493. checks = Check.objects.filter(project=request.project).order_by("created")
  494. ctx = {"checks": checks, "assigned": assigned, "channel": channel}
  495. return render(request, "front/channel_checks.html", ctx)
  496. @require_POST
  497. @login_required
  498. def update_channel_name(request, code):
  499. channel = get_object_or_404(Channel, code=code)
  500. if channel.project_id != request.project.id:
  501. return HttpResponseForbidden()
  502. form = ChannelNameForm(request.POST)
  503. if form.is_valid():
  504. channel.name = form.cleaned_data["name"]
  505. channel.save()
  506. return redirect("hc-channels")
  507. def verify_email(request, code, token):
  508. channel = get_object_or_404(Channel, code=code)
  509. if channel.make_token() == token:
  510. channel.email_verified = True
  511. channel.save()
  512. return render(request, "front/verify_email_success.html")
  513. return render(request, "bad_link.html")
  514. def unsubscribe_email(request, code, token):
  515. channel = get_object_or_404(Channel, code=code)
  516. if channel.make_token() != token:
  517. return render(request, "bad_link.html")
  518. if channel.kind != "email":
  519. return HttpResponseBadRequest()
  520. # Some email servers open links in emails to check for malicious content.
  521. # To work around this, we serve a form that auto-submits with JS.
  522. if "ask" in request.GET and request.method != "POST":
  523. return render(request, "accounts/unsubscribe_submit.html")
  524. channel.delete()
  525. return render(request, "front/unsubscribe_success.html")
  526. @require_POST
  527. @login_required
  528. def send_test_notification(request, code):
  529. channel = get_object_or_404(Channel, code=code)
  530. if channel.project_id != request.project.id:
  531. return HttpResponseForbidden()
  532. dummy = Check(name="TEST", status="down")
  533. dummy.last_ping = timezone.now() - td(days=1)
  534. dummy.n_pings = 42
  535. if channel.kind == "email":
  536. error = channel.transport.notify(dummy, channel.get_unsub_link())
  537. else:
  538. error = channel.transport.notify(dummy)
  539. if error:
  540. messages.warning(request, "Could not send a test notification: %s" % error)
  541. else:
  542. messages.success(request, "Test notification sent!")
  543. return redirect("hc-channels")
  544. @require_POST
  545. @login_required
  546. def remove_channel(request, code):
  547. # user may refresh the page during POST and cause two deletion attempts
  548. channel = Channel.objects.filter(code=code).first()
  549. if channel:
  550. if channel.project_id != request.project.id:
  551. return HttpResponseForbidden()
  552. channel.delete()
  553. return redirect("hc-channels")
  554. @login_required
  555. def add_email(request):
  556. if request.method == "POST":
  557. form = AddEmailForm(request.POST)
  558. if form.is_valid():
  559. channel = Channel(project=request.project, kind="email")
  560. channel.value = json.dumps(
  561. {
  562. "value": form.cleaned_data["value"],
  563. "up": form.cleaned_data["up"],
  564. "down": form.cleaned_data["down"],
  565. }
  566. )
  567. channel.save()
  568. channel.assign_all_checks()
  569. is_own_email = form.cleaned_data["value"] == request.user.email
  570. if is_own_email or not settings.EMAIL_USE_VERIFICATION:
  571. # If user is subscribing *their own* address
  572. # we can skip the verification step.
  573. # Additionally, in self-hosted setting, administator has the
  574. # option to disable the email verification step altogether.
  575. channel.email_verified = True
  576. channel.save()
  577. else:
  578. channel.send_verify_link()
  579. return redirect("hc-channels")
  580. else:
  581. form = AddEmailForm()
  582. ctx = {
  583. "page": "channels",
  584. "project": request.project,
  585. "use_verification": settings.EMAIL_USE_VERIFICATION,
  586. "form": form,
  587. }
  588. return render(request, "integrations/add_email.html", ctx)
  589. @login_required
  590. def add_webhook(request):
  591. if request.method == "POST":
  592. form = AddWebhookForm(request.POST)
  593. if form.is_valid():
  594. channel = Channel(project=request.project, kind="webhook")
  595. channel.value = form.get_value()
  596. channel.save()
  597. channel.assign_all_checks()
  598. return redirect("hc-channels")
  599. else:
  600. form = AddWebhookForm()
  601. ctx = {
  602. "page": "channels",
  603. "project": request.project,
  604. "form": form,
  605. "now": timezone.now().replace(microsecond=0).isoformat(),
  606. }
  607. return render(request, "integrations/add_webhook.html", ctx)
  608. def _prepare_state(request, session_key):
  609. state = get_random_string()
  610. request.session[session_key] = state
  611. return state
  612. def _get_validated_code(request, session_key, key="code"):
  613. if session_key not in request.session:
  614. return None
  615. session_state = request.session.pop(session_key)
  616. request_state = request.GET.get("state")
  617. if session_state is None or session_state != request_state:
  618. return None
  619. return request.GET.get(key)
  620. def add_pd(request, state=None):
  621. if settings.PD_VENDOR_KEY is None:
  622. raise Http404("pagerduty integration is not available")
  623. if state and request.user.is_authenticated:
  624. if "pd" not in request.session:
  625. return HttpResponseBadRequest()
  626. session_state = request.session.pop("pd")
  627. if session_state != state:
  628. return HttpResponseBadRequest()
  629. if request.GET.get("error") == "cancelled":
  630. messages.warning(request, "PagerDuty setup was cancelled")
  631. return redirect("hc-channels")
  632. channel = Channel(kind="pd", project=request.project)
  633. channel.user = request.project.owner
  634. channel.value = json.dumps(
  635. {
  636. "service_key": request.GET.get("service_key"),
  637. "account": request.GET.get("account"),
  638. }
  639. )
  640. channel.save()
  641. channel.assign_all_checks()
  642. messages.success(request, "The PagerDuty integration has been added!")
  643. return redirect("hc-channels")
  644. state = _prepare_state(request, "pd")
  645. callback = settings.SITE_ROOT + reverse("hc-add-pd-state", args=[state])
  646. connect_url = "https://connect.pagerduty.com/connect?" + urlencode(
  647. {"vendor": settings.PD_VENDOR_KEY, "callback": callback}
  648. )
  649. ctx = {"page": "channels", "project": request.project, "connect_url": connect_url}
  650. return render(request, "integrations/add_pd.html", ctx)
  651. @login_required
  652. def add_pagertree(request):
  653. if request.method == "POST":
  654. form = AddUrlForm(request.POST)
  655. if form.is_valid():
  656. channel = Channel(project=request.project, kind="pagertree")
  657. channel.value = form.cleaned_data["value"]
  658. channel.save()
  659. channel.assign_all_checks()
  660. return redirect("hc-channels")
  661. else:
  662. form = AddUrlForm()
  663. ctx = {"page": "channels", "project": request.project, "form": form}
  664. return render(request, "integrations/add_pagertree.html", ctx)
  665. @login_required
  666. def add_pagerteam(request):
  667. if request.method == "POST":
  668. form = AddUrlForm(request.POST)
  669. if form.is_valid():
  670. channel = Channel(project=request.project, kind="pagerteam")
  671. channel.value = form.cleaned_data["value"]
  672. channel.save()
  673. channel.assign_all_checks()
  674. return redirect("hc-channels")
  675. else:
  676. form = AddUrlForm()
  677. ctx = {"page": "channels", "project": request.project, "form": form}
  678. return render(request, "integrations/add_pagerteam.html", ctx)
  679. def add_slack(request):
  680. if not settings.SLACK_CLIENT_ID and not request.user.is_authenticated:
  681. return redirect("hc-login")
  682. if request.method == "POST":
  683. form = AddUrlForm(request.POST)
  684. if form.is_valid():
  685. channel = Channel(project=request.project, kind="slack")
  686. channel.value = form.cleaned_data["value"]
  687. channel.save()
  688. channel.assign_all_checks()
  689. return redirect("hc-channels")
  690. else:
  691. form = AddUrlForm()
  692. ctx = {
  693. "page": "channels",
  694. "form": form,
  695. "slack_client_id": settings.SLACK_CLIENT_ID,
  696. }
  697. if request.user.is_authenticated:
  698. ctx["project"] = request.project
  699. if settings.SLACK_CLIENT_ID and request.user.is_authenticated:
  700. ctx["state"] = _prepare_state(request, "slack")
  701. return render(request, "integrations/add_slack.html", ctx)
  702. @login_required
  703. def add_mattermost(request):
  704. if request.method == "POST":
  705. form = AddUrlForm(request.POST)
  706. if form.is_valid():
  707. channel = Channel(project=request.project, kind="mattermost")
  708. channel.value = form.cleaned_data["value"]
  709. channel.save()
  710. channel.assign_all_checks()
  711. return redirect("hc-channels")
  712. else:
  713. form = AddUrlForm()
  714. ctx = {"page": "channels", "form": form, "project": request.project}
  715. return render(request, "integrations/add_mattermost.html", ctx)
  716. @login_required
  717. def add_slack_btn(request):
  718. code = _get_validated_code(request, "slack")
  719. if code is None:
  720. return HttpResponseBadRequest()
  721. result = requests.post(
  722. "https://slack.com/api/oauth.access",
  723. {
  724. "client_id": settings.SLACK_CLIENT_ID,
  725. "client_secret": settings.SLACK_CLIENT_SECRET,
  726. "code": code,
  727. },
  728. )
  729. doc = result.json()
  730. if doc.get("ok"):
  731. channel = Channel(kind="slack", project=request.project)
  732. channel.user = request.project.owner
  733. channel.value = result.text
  734. channel.save()
  735. channel.assign_all_checks()
  736. messages.success(request, "The Slack integration has been added!")
  737. else:
  738. s = doc.get("error")
  739. messages.warning(request, "Error message from slack: %s" % s)
  740. return redirect("hc-channels")
  741. @login_required
  742. def add_pushbullet(request):
  743. if settings.PUSHBULLET_CLIENT_ID is None:
  744. raise Http404("pushbullet integration is not available")
  745. if "code" in request.GET:
  746. code = _get_validated_code(request, "pushbullet")
  747. if code is None:
  748. return HttpResponseBadRequest()
  749. result = requests.post(
  750. "https://api.pushbullet.com/oauth2/token",
  751. {
  752. "client_id": settings.PUSHBULLET_CLIENT_ID,
  753. "client_secret": settings.PUSHBULLET_CLIENT_SECRET,
  754. "code": code,
  755. "grant_type": "authorization_code",
  756. },
  757. )
  758. doc = result.json()
  759. if "access_token" in doc:
  760. channel = Channel(kind="pushbullet", project=request.project)
  761. channel.user = request.project.owner
  762. channel.value = doc["access_token"]
  763. channel.save()
  764. channel.assign_all_checks()
  765. messages.success(request, "The Pushbullet integration has been added!")
  766. else:
  767. messages.warning(request, "Something went wrong")
  768. return redirect("hc-channels")
  769. redirect_uri = settings.SITE_ROOT + reverse("hc-add-pushbullet")
  770. authorize_url = "https://www.pushbullet.com/authorize?" + urlencode(
  771. {
  772. "client_id": settings.PUSHBULLET_CLIENT_ID,
  773. "redirect_uri": redirect_uri,
  774. "response_type": "code",
  775. "state": _prepare_state(request, "pushbullet"),
  776. }
  777. )
  778. ctx = {
  779. "page": "channels",
  780. "project": request.project,
  781. "authorize_url": authorize_url,
  782. }
  783. return render(request, "integrations/add_pushbullet.html", ctx)
  784. @login_required
  785. def add_discord(request):
  786. if settings.DISCORD_CLIENT_ID is None:
  787. raise Http404("discord integration is not available")
  788. redirect_uri = settings.SITE_ROOT + reverse("hc-add-discord")
  789. if "code" in request.GET:
  790. code = _get_validated_code(request, "discord")
  791. if code is None:
  792. return HttpResponseBadRequest()
  793. result = requests.post(
  794. "https://discordapp.com/api/oauth2/token",
  795. {
  796. "client_id": settings.DISCORD_CLIENT_ID,
  797. "client_secret": settings.DISCORD_CLIENT_SECRET,
  798. "code": code,
  799. "grant_type": "authorization_code",
  800. "redirect_uri": redirect_uri,
  801. },
  802. )
  803. doc = result.json()
  804. if "access_token" in doc:
  805. channel = Channel(kind="discord", project=request.project)
  806. channel.user = request.project.owner
  807. channel.value = result.text
  808. channel.save()
  809. channel.assign_all_checks()
  810. messages.success(request, "The Discord integration has been added!")
  811. else:
  812. messages.warning(request, "Something went wrong")
  813. return redirect("hc-channels")
  814. auth_url = "https://discordapp.com/api/oauth2/authorize?" + urlencode(
  815. {
  816. "client_id": settings.DISCORD_CLIENT_ID,
  817. "scope": "webhook.incoming",
  818. "redirect_uri": redirect_uri,
  819. "response_type": "code",
  820. "state": _prepare_state(request, "discord"),
  821. }
  822. )
  823. ctx = {"page": "channels", "project": request.project, "authorize_url": auth_url}
  824. return render(request, "integrations/add_discord.html", ctx)
  825. def add_pushover(request):
  826. if (
  827. settings.PUSHOVER_API_TOKEN is None
  828. or settings.PUSHOVER_SUBSCRIPTION_URL is None
  829. ):
  830. raise Http404("pushover integration is not available")
  831. if not request.user.is_authenticated:
  832. ctx = {"page": "channels"}
  833. return render(request, "integrations/add_pushover.html", ctx)
  834. if request.method == "POST":
  835. # Initiate the subscription
  836. state = _prepare_state(request, "pushover")
  837. failure_url = settings.SITE_ROOT + reverse("hc-channels")
  838. success_url = (
  839. settings.SITE_ROOT
  840. + reverse("hc-add-pushover")
  841. + "?"
  842. + urlencode(
  843. {
  844. "state": state,
  845. "prio": request.POST.get("po_priority", "0"),
  846. "prio_up": request.POST.get("po_priority_up", "0"),
  847. }
  848. )
  849. )
  850. subscription_url = (
  851. settings.PUSHOVER_SUBSCRIPTION_URL
  852. + "?"
  853. + urlencode({"success": success_url, "failure": failure_url})
  854. )
  855. return redirect(subscription_url)
  856. # Handle successful subscriptions
  857. if "pushover_user_key" in request.GET:
  858. key = _get_validated_code(request, "pushover", "pushover_user_key")
  859. if key is None:
  860. return HttpResponseBadRequest()
  861. # Validate priority
  862. prio = request.GET.get("prio")
  863. if prio not in ("-2", "-1", "0", "1", "2"):
  864. return HttpResponseBadRequest()
  865. prio_up = request.GET.get("prio_up")
  866. if prio_up not in ("-2", "-1", "0", "1", "2"):
  867. return HttpResponseBadRequest()
  868. if request.GET.get("pushover_unsubscribed") == "1":
  869. # Unsubscription: delete all Pushover channels for this project
  870. Channel.objects.filter(project=request.project, kind="po").delete()
  871. return redirect("hc-channels")
  872. # Subscription
  873. channel = Channel(project=request.project, kind="po")
  874. channel.value = "%s|%s|%s" % (key, prio, prio_up)
  875. channel.save()
  876. channel.assign_all_checks()
  877. messages.success(request, "The Pushover integration has been added!")
  878. return redirect("hc-channels")
  879. # Show Integration Settings form
  880. ctx = {
  881. "page": "channels",
  882. "project": request.project,
  883. "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY),
  884. "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION),
  885. }
  886. return render(request, "integrations/add_pushover.html", ctx)
  887. @login_required
  888. def add_opsgenie(request):
  889. if request.method == "POST":
  890. form = AddOpsGenieForm(request.POST)
  891. if form.is_valid():
  892. channel = Channel(project=request.project, kind="opsgenie")
  893. channel.value = form.cleaned_data["value"]
  894. channel.save()
  895. channel.assign_all_checks()
  896. return redirect("hc-channels")
  897. else:
  898. form = AddUrlForm()
  899. ctx = {"page": "channels", "project": request.project, "form": form}
  900. return render(request, "integrations/add_opsgenie.html", ctx)
  901. @login_required
  902. def add_victorops(request):
  903. if request.method == "POST":
  904. form = AddUrlForm(request.POST)
  905. if form.is_valid():
  906. channel = Channel(project=request.project, kind="victorops")
  907. channel.value = form.cleaned_data["value"]
  908. channel.save()
  909. channel.assign_all_checks()
  910. return redirect("hc-channels")
  911. else:
  912. form = AddUrlForm()
  913. ctx = {"page": "channels", "project": request.project, "form": form}
  914. return render(request, "integrations/add_victorops.html", ctx)
  915. @csrf_exempt
  916. @require_POST
  917. def telegram_bot(request):
  918. try:
  919. doc = json.loads(request.body.decode())
  920. jsonschema.validate(doc, telegram_callback)
  921. except ValueError:
  922. return HttpResponseBadRequest()
  923. except jsonschema.ValidationError:
  924. # We don't recognize the message format, but don't want Telegram
  925. # retrying this over and over again, so respond with 200 OK
  926. return HttpResponse()
  927. if "/start" not in doc["message"]["text"]:
  928. return HttpResponse()
  929. chat = doc["message"]["chat"]
  930. name = max(chat.get("title", ""), chat.get("username", ""))
  931. invite = render_to_string(
  932. "integrations/telegram_invite.html",
  933. {"qs": signing.dumps((chat["id"], chat["type"], name))},
  934. )
  935. Telegram.send(chat["id"], invite)
  936. return HttpResponse()
  937. @login_required
  938. def add_telegram(request):
  939. chat_id, chat_type, chat_name = None, None, None
  940. qs = request.META["QUERY_STRING"]
  941. if qs:
  942. chat_id, chat_type, chat_name = signing.loads(qs, max_age=600)
  943. if request.method == "POST":
  944. channel = Channel(project=request.project, kind="telegram")
  945. channel.value = json.dumps(
  946. {"id": chat_id, "type": chat_type, "name": chat_name}
  947. )
  948. channel.save()
  949. channel.assign_all_checks()
  950. messages.success(request, "The Telegram integration has been added!")
  951. return redirect("hc-channels")
  952. ctx = {
  953. "page": "channels",
  954. "project": request.project,
  955. "chat_id": chat_id,
  956. "chat_type": chat_type,
  957. "chat_name": chat_name,
  958. "bot_name": settings.TELEGRAM_BOT_NAME,
  959. }
  960. return render(request, "integrations/add_telegram.html", ctx)
  961. @login_required
  962. def add_sms(request):
  963. if settings.TWILIO_AUTH is None:
  964. raise Http404("sms integration is not available")
  965. if request.method == "POST":
  966. form = AddSmsForm(request.POST)
  967. if form.is_valid():
  968. channel = Channel(project=request.project, kind="sms")
  969. channel.name = form.cleaned_data["label"]
  970. channel.value = json.dumps({"value": form.cleaned_data["value"]})
  971. channel.save()
  972. channel.assign_all_checks()
  973. return redirect("hc-channels")
  974. else:
  975. form = AddSmsForm()
  976. ctx = {
  977. "page": "channels",
  978. "project": request.project,
  979. "form": form,
  980. "profile": request.project.owner_profile,
  981. }
  982. return render(request, "integrations/add_sms.html", ctx)
  983. @login_required
  984. def add_whatsapp(request):
  985. if not settings.TWILIO_USE_WHATSAPP:
  986. raise Http404("whatsapp integration is not available")
  987. if request.method == "POST":
  988. form = AddSmsForm(request.POST)
  989. if form.is_valid():
  990. channel = Channel(project=request.project, kind="whatsapp")
  991. channel.name = form.cleaned_data["label"]
  992. channel.value = json.dumps(
  993. {
  994. "value": form.cleaned_data["value"],
  995. "up": form.cleaned_data["up"],
  996. "down": form.cleaned_data["down"],
  997. }
  998. )
  999. channel.save()
  1000. channel.assign_all_checks()
  1001. return redirect("hc-channels")
  1002. else:
  1003. form = AddSmsForm()
  1004. ctx = {
  1005. "page": "channels",
  1006. "project": request.project,
  1007. "form": form,
  1008. "profile": request.project.owner_profile,
  1009. }
  1010. return render(request, "integrations/add_whatsapp.html", ctx)
  1011. @login_required
  1012. def add_trello(request):
  1013. if settings.TRELLO_APP_KEY is None:
  1014. raise Http404("trello integration is not available")
  1015. if request.method == "POST":
  1016. channel = Channel(project=request.project, kind="trello")
  1017. channel.value = request.POST["settings"]
  1018. channel.save()
  1019. channel.assign_all_checks()
  1020. return redirect("hc-channels")
  1021. authorize_url = "https://trello.com/1/authorize?" + urlencode(
  1022. {
  1023. "expiration": "never",
  1024. "name": settings.SITE_NAME,
  1025. "scope": "read,write",
  1026. "response_type": "token",
  1027. "key": settings.TRELLO_APP_KEY,
  1028. "return_url": settings.SITE_ROOT + reverse("hc-add-trello"),
  1029. }
  1030. )
  1031. ctx = {
  1032. "page": "channels",
  1033. "project": request.project,
  1034. "authorize_url": authorize_url,
  1035. }
  1036. return render(request, "integrations/add_trello.html", ctx)
  1037. @login_required
  1038. def add_matrix(request):
  1039. if settings.MATRIX_ACCESS_TOKEN is None:
  1040. raise Http404("matrix integration is not available")
  1041. if request.method == "POST":
  1042. form = AddMatrixForm(request.POST)
  1043. if form.is_valid():
  1044. channel = Channel(project=request.project, kind="matrix")
  1045. channel.value = form.cleaned_data["room_id"]
  1046. # If user supplied room alias instead of ID, use it as channel name
  1047. alias = form.cleaned_data["alias"]
  1048. if not alias.startswith("!"):
  1049. channel.name = alias
  1050. channel.save()
  1051. channel.assign_all_checks()
  1052. messages.success(request, "The Matrix integration has been added!")
  1053. return redirect("hc-channels")
  1054. else:
  1055. form = AddMatrixForm()
  1056. ctx = {
  1057. "page": "channels",
  1058. "project": request.project,
  1059. "form": form,
  1060. "matrix_user_id": settings.MATRIX_USER_ID,
  1061. }
  1062. return render(request, "integrations/add_matrix.html", ctx)
  1063. @login_required
  1064. def add_apprise(request):
  1065. if not settings.APPRISE_ENABLED:
  1066. raise Http404("apprise integration is not available")
  1067. if request.method == "POST":
  1068. form = AddAppriseForm(request.POST)
  1069. if form.is_valid():
  1070. channel = Channel(project=request.project, kind="apprise")
  1071. channel.value = form.cleaned_data["url"]
  1072. channel.save()
  1073. channel.assign_all_checks()
  1074. messages.success(request, "The Apprise integration has been added!")
  1075. return redirect("hc-channels")
  1076. else:
  1077. form = AddAppriseForm()
  1078. ctx = {"page": "channels", "project": request.project, "form": form}
  1079. return render(request, "integrations/add_apprise.html", ctx)
  1080. @login_required
  1081. @require_POST
  1082. def trello_settings(request):
  1083. token = request.POST.get("token")
  1084. url = "https://api.trello.com/1/members/me/boards?" + urlencode(
  1085. {
  1086. "key": settings.TRELLO_APP_KEY,
  1087. "token": token,
  1088. "fields": "id,name",
  1089. "lists": "open",
  1090. "list_fields": "id,name",
  1091. }
  1092. )
  1093. r = requests.get(url)
  1094. ctx = {"token": token, "data": r.json()}
  1095. return render(request, "integrations/trello_settings.html", ctx)