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.

554 lines
16 KiB

9 years ago
8 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
9 years ago
8 years ago
9 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
8 years ago
  1. from collections import Counter
  2. from datetime import timedelta as td
  3. from itertools import tee
  4. import requests
  5. from django.conf import settings
  6. from django.contrib import messages
  7. from django.contrib.auth.decorators import login_required
  8. from django.db.models import Count
  9. from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden
  10. from django.shortcuts import get_object_or_404, redirect, render
  11. from django.urls import reverse
  12. from django.utils import timezone
  13. from django.utils.crypto import get_random_string
  14. from django.utils.six.moves.urllib.parse import urlencode
  15. from hc.api.decorators import uuid_or_400
  16. from hc.api.models import DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check, Ping
  17. from hc.front.forms import (AddChannelForm, AddWebhookForm, NameTagsForm,
  18. TimeoutForm)
  19. # from itertools recipes:
  20. def pairwise(iterable):
  21. "s -> (s0,s1), (s1,s2), (s2, s3), ..."
  22. a, b = tee(iterable)
  23. next(b, None)
  24. return zip(a, b)
  25. @login_required
  26. def my_checks(request):
  27. q = Check.objects.filter(user=request.team.user).order_by("created")
  28. checks = list(q)
  29. counter = Counter()
  30. down_tags, grace_tags = set(), set()
  31. for check in checks:
  32. status = check.get_status()
  33. for tag in check.tags_list():
  34. if tag == "":
  35. continue
  36. counter[tag] += 1
  37. if status == "down":
  38. down_tags.add(tag)
  39. elif check.in_grace_period():
  40. grace_tags.add(tag)
  41. ctx = {
  42. "page": "checks",
  43. "checks": checks,
  44. "now": timezone.now(),
  45. "tags": counter.most_common(),
  46. "down_tags": down_tags,
  47. "grace_tags": grace_tags,
  48. "ping_endpoint": settings.PING_ENDPOINT
  49. }
  50. return render(request, "front/my_checks.html", ctx)
  51. def _welcome_check(request):
  52. check = None
  53. if "welcome_code" in request.session:
  54. code = request.session["welcome_code"]
  55. check = Check.objects.filter(code=code).first()
  56. if check is None:
  57. check = Check()
  58. check.save()
  59. request.session["welcome_code"] = str(check.code)
  60. return check
  61. def index(request):
  62. if request.user.is_authenticated:
  63. return redirect("hc-checks")
  64. check = _welcome_check(request)
  65. ctx = {
  66. "page": "welcome",
  67. "check": check,
  68. "ping_url": check.url(),
  69. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None
  70. }
  71. return render(request, "front/welcome.html", ctx)
  72. def docs(request):
  73. check = _welcome_check(request)
  74. ctx = {
  75. "page": "docs",
  76. "section": "home",
  77. "ping_endpoint": settings.PING_ENDPOINT,
  78. "check": check,
  79. "ping_url": check.url()
  80. }
  81. return render(request, "front/docs.html", ctx)
  82. def docs_api(request):
  83. ctx = {
  84. "page": "docs",
  85. "section": "api",
  86. "SITE_ROOT": settings.SITE_ROOT,
  87. "PING_ENDPOINT": settings.PING_ENDPOINT,
  88. "default_timeout": int(DEFAULT_TIMEOUT.total_seconds()),
  89. "default_grace": int(DEFAULT_GRACE.total_seconds())
  90. }
  91. return render(request, "front/docs_api.html", ctx)
  92. def about(request):
  93. return render(request, "front/about.html", {"page": "about"})
  94. @login_required
  95. def add_check(request):
  96. assert request.method == "POST"
  97. check = Check(user=request.team.user)
  98. check.save()
  99. check.assign_all_channels()
  100. return redirect("hc-checks")
  101. @login_required
  102. @uuid_or_400
  103. def update_name(request, code):
  104. assert request.method == "POST"
  105. check = get_object_or_404(Check, code=code)
  106. if check.user_id != request.team.user.id:
  107. return HttpResponseForbidden()
  108. form = NameTagsForm(request.POST)
  109. if form.is_valid():
  110. check.name = form.cleaned_data["name"]
  111. check.tags = form.cleaned_data["tags"]
  112. check.save()
  113. return redirect("hc-checks")
  114. @login_required
  115. @uuid_or_400
  116. def update_timeout(request, code):
  117. assert request.method == "POST"
  118. check = get_object_or_404(Check, code=code)
  119. if check.user != request.team.user:
  120. return HttpResponseForbidden()
  121. form = TimeoutForm(request.POST)
  122. if form.is_valid():
  123. check.timeout = td(seconds=form.cleaned_data["timeout"])
  124. check.grace = td(seconds=form.cleaned_data["grace"])
  125. check.save()
  126. return redirect("hc-checks")
  127. @login_required
  128. @uuid_or_400
  129. def pause(request, code):
  130. assert request.method == "POST"
  131. check = get_object_or_404(Check, code=code)
  132. if check.user_id != request.team.user.id:
  133. return HttpResponseForbidden()
  134. check.status = "paused"
  135. check.save()
  136. return redirect("hc-checks")
  137. @login_required
  138. @uuid_or_400
  139. def remove_check(request, code):
  140. assert request.method == "POST"
  141. check = get_object_or_404(Check, code=code)
  142. if check.user != request.team.user:
  143. return HttpResponseForbidden()
  144. check.delete()
  145. return redirect("hc-checks")
  146. @login_required
  147. @uuid_or_400
  148. def log(request, code):
  149. check = get_object_or_404(Check, code=code)
  150. if check.user != request.team.user:
  151. return HttpResponseForbidden()
  152. limit = request.team.ping_log_limit
  153. pings = Ping.objects.filter(owner=check).order_by("-id")[:limit]
  154. pings = list(pings.iterator())
  155. # oldest-to-newest order will be more convenient for adding
  156. # "not received" placeholders:
  157. pings.reverse()
  158. # Add a dummy ping object at the end. We iterate over *pairs* of pings
  159. # and don't want to handle a special case of a check with a single ping.
  160. pings.append(Ping(created=timezone.now()))
  161. # Now go through pings, calculate time gaps, and decorate
  162. # the pings list for convenient use in template
  163. wrapped = []
  164. early = False
  165. for older, newer in pairwise(pings):
  166. wrapped.append({"ping": older, "early": early})
  167. # Fill in "missed ping" placeholders:
  168. expected_date = older.created + check.timeout
  169. n_blanks = 0
  170. while expected_date + check.grace < newer.created and n_blanks < 10:
  171. wrapped.append({"placeholder_date": expected_date})
  172. expected_date = expected_date + check.timeout
  173. n_blanks += 1
  174. # Prepare early flag for next ping to come
  175. early = older.created + check.timeout > newer.created + check.grace
  176. reached_limit = len(pings) > limit
  177. wrapped.reverse()
  178. ctx = {
  179. "check": check,
  180. "pings": wrapped,
  181. "num_pings": len(pings),
  182. "limit": limit,
  183. "show_limit_notice": reached_limit and settings.USE_PAYMENTS
  184. }
  185. return render(request, "front/log.html", ctx)
  186. @login_required
  187. def channels(request):
  188. if request.method == "POST":
  189. code = request.POST["channel"]
  190. try:
  191. channel = Channel.objects.get(code=code)
  192. except Channel.DoesNotExist:
  193. return HttpResponseBadRequest()
  194. if channel.user_id != request.team.user.id:
  195. return HttpResponseForbidden()
  196. new_checks = []
  197. for key in request.POST:
  198. if key.startswith("check-"):
  199. code = key[6:]
  200. try:
  201. check = Check.objects.get(code=code)
  202. except Check.DoesNotExist:
  203. return HttpResponseBadRequest()
  204. if check.user_id != request.team.user.id:
  205. return HttpResponseForbidden()
  206. new_checks.append(check)
  207. channel.checks = new_checks
  208. return redirect("hc-channels")
  209. channels = Channel.objects.filter(user=request.team.user).order_by("created")
  210. channels = channels.annotate(n_checks=Count("checks"))
  211. num_checks = Check.objects.filter(user=request.team.user).count()
  212. ctx = {
  213. "page": "channels",
  214. "channels": channels,
  215. "num_checks": num_checks,
  216. "enable_pushbullet": settings.PUSHBULLET_CLIENT_ID is not None,
  217. "enable_pushover": settings.PUSHOVER_API_TOKEN is not None
  218. }
  219. return render(request, "front/channels.html", ctx)
  220. def do_add_channel(request, data):
  221. form = AddChannelForm(data)
  222. if form.is_valid():
  223. channel = form.save(commit=False)
  224. channel.user = request.team.user
  225. channel.save()
  226. channel.assign_all_checks()
  227. if channel.kind == "email":
  228. channel.send_verify_link()
  229. return redirect("hc-channels")
  230. else:
  231. return HttpResponseBadRequest()
  232. @login_required
  233. def add_channel(request):
  234. assert request.method == "POST"
  235. return do_add_channel(request, request.POST)
  236. @login_required
  237. @uuid_or_400
  238. def channel_checks(request, code):
  239. channel = get_object_or_404(Channel, code=code)
  240. if channel.user_id != request.team.user.id:
  241. return HttpResponseForbidden()
  242. assigned = set(channel.checks.values_list('code', flat=True).distinct())
  243. checks = Check.objects.filter(user=request.team.user).order_by("created")
  244. ctx = {
  245. "checks": checks,
  246. "assigned": assigned,
  247. "channel": channel
  248. }
  249. return render(request, "front/channel_checks.html", ctx)
  250. @uuid_or_400
  251. def verify_email(request, code, token):
  252. channel = get_object_or_404(Channel, code=code)
  253. if channel.make_token() == token:
  254. channel.email_verified = True
  255. channel.save()
  256. return render(request, "front/verify_email_success.html")
  257. return render(request, "bad_link.html")
  258. @login_required
  259. @uuid_or_400
  260. def remove_channel(request, code):
  261. assert request.method == "POST"
  262. # user may refresh the page during POST and cause two deletion attempts
  263. channel = Channel.objects.filter(code=code).first()
  264. if channel:
  265. if channel.user != request.team.user:
  266. return HttpResponseForbidden()
  267. channel.delete()
  268. return redirect("hc-channels")
  269. @login_required
  270. def add_email(request):
  271. ctx = {"page": "channels"}
  272. return render(request, "integrations/add_email.html", ctx)
  273. @login_required
  274. def add_webhook(request):
  275. if request.method == "POST":
  276. form = AddWebhookForm(request.POST)
  277. if form.is_valid():
  278. channel = Channel(user=request.team.user, kind="webhook")
  279. channel.value = form.get_value()
  280. channel.save()
  281. channel.assign_all_checks()
  282. return redirect("hc-channels")
  283. else:
  284. form = AddWebhookForm()
  285. ctx = {"page": "channels", "form": form}
  286. return render(request, "integrations/add_webhook.html", ctx)
  287. @login_required
  288. def add_pd(request):
  289. ctx = {"page": "channels"}
  290. return render(request, "integrations/add_pd.html", ctx)
  291. def add_slack(request):
  292. if not settings.SLACK_CLIENT_ID and not request.user.is_authenticated:
  293. return redirect("hc-login")
  294. ctx = {
  295. "page": "channels",
  296. "slack_client_id": settings.SLACK_CLIENT_ID
  297. }
  298. return render(request, "integrations/add_slack.html", ctx)
  299. @login_required
  300. def add_slack_btn(request):
  301. code = request.GET.get("code", "")
  302. if len(code) < 8:
  303. return HttpResponseBadRequest()
  304. result = requests.post("https://slack.com/api/oauth.access", {
  305. "client_id": settings.SLACK_CLIENT_ID,
  306. "client_secret": settings.SLACK_CLIENT_SECRET,
  307. "code": code
  308. })
  309. doc = result.json()
  310. if doc.get("ok"):
  311. channel = Channel()
  312. channel.user = request.team.user
  313. channel.kind = "slack"
  314. channel.value = result.text
  315. channel.save()
  316. channel.assign_all_checks()
  317. messages.success(request, "The Slack integration has been added!")
  318. else:
  319. s = doc.get("error")
  320. messages.warning(request, "Error message from slack: %s" % s)
  321. return redirect("hc-channels")
  322. @login_required
  323. def add_hipchat(request):
  324. ctx = {"page": "channels"}
  325. return render(request, "integrations/add_hipchat.html", ctx)
  326. @login_required
  327. def add_pushbullet(request):
  328. if settings.PUSHBULLET_CLIENT_ID is None:
  329. raise Http404("pushbullet integration is not available")
  330. if "code" in request.GET:
  331. code = request.GET.get("code", "")
  332. if len(code) < 8:
  333. return HttpResponseBadRequest()
  334. result = requests.post("https://api.pushbullet.com/oauth2/token", {
  335. "client_id": settings.PUSHBULLET_CLIENT_ID,
  336. "client_secret": settings.PUSHBULLET_CLIENT_SECRET,
  337. "code": code,
  338. "grant_type": "authorization_code"
  339. })
  340. doc = result.json()
  341. if "access_token" in doc:
  342. channel = Channel(kind="pushbullet")
  343. channel.user = request.team.user
  344. channel.value = doc["access_token"]
  345. channel.save()
  346. channel.assign_all_checks()
  347. messages.success(request,
  348. "The Pushbullet integration has been added!")
  349. else:
  350. messages.warning(request, "Something went wrong")
  351. return redirect("hc-channels")
  352. redirect_uri = settings.SITE_ROOT + reverse("hc-add-pushbullet")
  353. authorize_url = "https://www.pushbullet.com/authorize?" + urlencode({
  354. "client_id": settings.PUSHBULLET_CLIENT_ID,
  355. "redirect_uri": redirect_uri,
  356. "response_type": "code"
  357. })
  358. ctx = {
  359. "page": "channels",
  360. "authorize_url": authorize_url
  361. }
  362. return render(request, "integrations/add_pushbullet.html", ctx)
  363. @login_required
  364. def add_pushover(request):
  365. if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None:
  366. raise Http404("pushover integration is not available")
  367. if request.method == "POST":
  368. # Initiate the subscription
  369. nonce = get_random_string()
  370. request.session["po_nonce"] = nonce
  371. failure_url = settings.SITE_ROOT + reverse("hc-channels")
  372. success_url = settings.SITE_ROOT + reverse("hc-add-pushover") + "?" + urlencode({
  373. "nonce": nonce,
  374. "prio": request.POST.get("po_priority", "0"),
  375. })
  376. subscription_url = settings.PUSHOVER_SUBSCRIPTION_URL + "?" + urlencode({
  377. "success": success_url,
  378. "failure": failure_url,
  379. })
  380. return redirect(subscription_url)
  381. # Handle successful subscriptions
  382. if "pushover_user_key" in request.GET:
  383. if "nonce" not in request.GET or "prio" not in request.GET:
  384. return HttpResponseBadRequest()
  385. # Validate nonce
  386. if request.GET["nonce"] != request.session.get("po_nonce"):
  387. return HttpResponseForbidden()
  388. # Validate priority
  389. if request.GET["prio"] not in ("-2", "-1", "0", "1", "2"):
  390. return HttpResponseBadRequest()
  391. # All looks well--
  392. del request.session["po_nonce"]
  393. if request.GET.get("pushover_unsubscribed") == "1":
  394. # Unsubscription: delete all Pushover channels for this user
  395. Channel.objects.filter(user=request.user, kind="po").delete()
  396. return redirect("hc-channels")
  397. else:
  398. # Subscription
  399. user_key = request.GET["pushover_user_key"]
  400. priority = int(request.GET["prio"])
  401. return do_add_channel(request, {
  402. "kind": "po",
  403. "value": "%s|%d" % (user_key, priority),
  404. })
  405. # Show Integration Settings form
  406. ctx = {
  407. "page": "channels",
  408. "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY),
  409. "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION),
  410. }
  411. return render(request, "integrations/add_pushover.html", ctx)
  412. @login_required
  413. def add_victorops(request):
  414. ctx = {"page": "channels"}
  415. return render(request, "integrations/add_victorops.html", ctx)
  416. def privacy(request):
  417. return render(request, "front/privacy.html", {})
  418. def terms(request):
  419. return render(request, "front/terms.html", {})