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.

1165 lines
35 KiB

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