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.

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