From 549c6444803425bc8a0261641910b68a160bf3cd Mon Sep 17 00:00:00 2001 From: Scott Feeney Date: Thu, 29 Aug 2013 18:12:04 -0400 Subject: [PATCH] Move webhooks views to a separate file (imported from commit 73db9f5ccc03d19024f35001b0805ca17eeadff7) --- zerver/views/__init__.py | 406 ------------------------------------- zerver/views/webhooks.py | 423 +++++++++++++++++++++++++++++++++++++++ zproject/urls.py | 12 +- 3 files changed, 429 insertions(+), 412 deletions(-) create mode 100644 zerver/views/webhooks.py diff --git a/zerver/views/__init__.py b/zerver/views/__init__.py index a91572bf23..10c9deae31 100644 --- a/zerver/views/__init__.py +++ b/zerver/views/__init__.py @@ -76,11 +76,8 @@ import base64 import time import logging from os import path -from functools import wraps from collections import defaultdict -from defusedxml.ElementTree import fromstring as xml_fromstring - def list_to_streams(streams_raw, user_profile, autocreate=False, invite_only=False): """Converts plaintext stream names to a list of Streams, validating input in the process @@ -1825,413 +1822,10 @@ def get_activity(request): 'iPhone': ActivityTable('iPhone', api_queries) }}, context_instance=RequestContext(request)) -def build_message_from_gitlog(user_profile, name, ref, commits, before, after, url, pusher): - short_ref = re.sub(r'^refs/heads/', '', ref) - subject = name - - if re.match(r'^0+$', after): - content = "%s deleted branch %s" % (pusher, - short_ref) - elif len(commits) == 0: - content = ("%s [force pushed](%s) to branch %s. Head is now %s" - % (pusher, - url, - short_ref, - after[:7])) - else: - content = build_commit_list_content(commits, short_ref, url, pusher) - - return (subject, content) - -def build_commit_list_content(commits, branch, compare_url, pusher): - if compare_url is not None: - push_text = "[pushed](%s)" % (compare_url,) - else: - push_text = "pushed" - content = ("%s %s to branch %s\n\n" - % (pusher, - push_text, - branch)) - num_commits = len(commits) - max_commits = 10 - truncated_commits = commits[:max_commits] - for commit in truncated_commits: - short_id = commit['id'][:7] - (short_commit_msg, _, _) = commit['message'].partition("\n") - content += "* [%s](%s): %s\n" % (short_id, commit['url'], - short_commit_msg) - if (num_commits > max_commits): - content += ("\n[and %d more commits]" - % (num_commits - max_commits,)) - - return content - -@authenticated_api_view -@has_request_variables -def api_github_landing(request, user_profile, event=REQ, - payload=REQ(converter=json_to_dict), - branches=REQ(default=''), - stream=REQ(default='')): - # TODO: this should all be moved to an external bot - repository = payload['repository'] - - # Special hook for capturing event data - try: - if repository['name'] == 'zulip-test': - with open('/var/log/humbug/github-payloads', 'a') as f: - f.write(ujson.dumps({'event': event, 'payload': payload})) - f.write("\n") - except Exception: - logging.exception("Error while capturing Github event") - - if not stream: - stream = 'commits' - - # CUSTOMER18 has requested not to get pull request notifications - if event == 'pull_request' and user_profile.realm.domain not in ['customer18.invalid', 'zulip.com']: - pull_req = payload['pull_request'] - - subject = "%s: pull request %d" % (repository['name'], - pull_req['number']) - content = ("Pull request from %s [%s](%s):\n\n %s\n\n> %s" - % (pull_req['user']['login'], - payload['action'], - pull_req['html_url'], - pull_req['title'], - pull_req['body'])) - elif event == 'push': - short_ref = re.sub(r'^refs/heads/', '', payload['ref']) - # This is a bit hackish, but is basically so that CUSTOMER18 doesn't - # get spammed when people commit to non-master all over the place. - # Long-term, this will be replaced by some GitHub configuration - # option of which branches to notify on. - if short_ref != 'master' and user_profile.realm.domain in ['customer18.invalid', 'zulip.com']: - return json_success() - - if branches: - # If we are given a whitelist of branches, then we silently ignore - # any push notification on a branch that is not in our whitelist. - if short_ref not in re.split('[\s,;|]+', branches): - return json_success() - - - subject, content = build_message_from_gitlog(user_profile, repository['name'], - payload['ref'], payload['commits'], - payload['before'], payload['after'], - payload['compare'], - payload['pusher']['name']) - elif event == 'issues': - if user_profile.realm.domain not in ('zulip.com', 'customer5.invalid'): - return json_success() - - issue = payload['issue'] - subject = "%s: issue %d %s" % (repository['name'], issue['number'], payload['action']) - content = ("%s %s [issue %d](%s): %s\n\n> %s" - % (issue['user']['login'], - payload['action'], - issue['number'], - issue['html_url'], - issue['title'], - issue['body'])) - stream = 'issues' - else: - # We don't handle other events even though we get notified - # about them - return json_success() - - subject = elide_subject(subject) - - request.client = get_client("github_bot") - return send_message_backend(request, user_profile, - message_type_name="stream", - message_to=[stream], - forged=False, subject_name=subject, - message_content=content) - -def elide_subject(subject): - if len(subject) > MAX_SUBJECT_LENGTH: - subject = subject[:57].rstrip() + '...' - return subject - -@csrf_exempt -def api_jira_webhook(request): - try: - api_key = request.GET['api_key'] - except (AttributeError, KeyError): - return json_error("Missing api_key parameter.") - - try: - payload = ujson.loads(request.body) - except ValueError: - return json_error("Malformed JSON input") - - try: - stream = request.GET['stream'] - except (AttributeError, KeyError): - stream = 'jira' - - try: - user_profile = UserProfile.objects.get(api_key=api_key) - request.user = user_profile - except UserProfile.DoesNotExist: - return json_error("Failed to find user with API key: %s" % (api_key,)) - - rate_limit_user(request, user_profile, domain='all') - - def get_in(payload, keys, default=''): - try: - for key in keys: - payload = payload[key] - except (AttributeError, KeyError): - return default - return payload - - event = payload.get('webhookEvent') - author = get_in(payload, ['user', 'displayName']) - issueId = get_in(payload, ['issue', 'key']) - # Guess the URL as it is not specified in the payload - # We assume that there is a /browse/BUG-### page - # from the REST url of the issue itself - baseUrl = re.match("(.*)\/rest\/api/.*", get_in(payload, ['issue', 'self'])) - if baseUrl and len(baseUrl.groups()): - issue = "[%s](%s/browse/%s)" % (issueId, baseUrl.group(1), issueId) - else: - issue = issueId - title = get_in(payload, ['issue', 'fields', 'summary']) - priority = get_in(payload, ['issue', 'fields', 'priority', 'name']) - assignee = get_in(payload, ['assignee', 'displayName'], 'no one') - subject = "%s: %s" % (issueId, title) - - if event == 'jira:issue_created': - content = "%s **created** %s priority %s, assigned to **%s**:\n\n> %s" % \ - (author, issue, priority, assignee, title) - elif event == 'jira:issue_deleted': - content = "%s **deleted** %s!" % \ - (author, issue) - elif event == 'jira:issue_updated': - # Reassigned, commented, reopened, and resolved events are all bundled - # into this one 'updated' event type, so we try to extract the meaningful - # event that happened - content = "%s **updated** %s:\n\n" % (author, issue) - changelog = get_in(payload, ['changelog',]) - comment = get_in(payload, ['comment', 'body']) - - if changelog != '': - # Use the changelog to display the changes, whitelist types we accept - items = changelog.get('items') - for item in items: - field = item.get('field') - if field in ('status', 'assignee'): - content += "* Changed %s from **%s** to **%s**\n" % (field, item.get('fromString'), item.get('toString')) - - if comment != '': - content += "\n> %s" % (comment,) - elif 'transition' in payload: - from_status = get_in(payload, ['transition', 'from_status']) - to_status = get_in(payload, ['transition', 'to_status']) - content = "%s **transitioned** %s from %s to %s" % (author, issue, from_status, to_status) - else: - # Unknown event type - if not settings.TEST_SUITE: - logging.warning("Got JIRA event type we don't understand: %s" % (event,)) - return json_error("Unknown JIRA event type") - - subject = elide_subject(subject) - - check_send_message(user_profile, get_client("API"), "stream", [stream], subject, content) - return json_success() - -@csrf_exempt -def api_pivotal_webhook(request): - try: - api_key = request.GET['api_key'] - stream = request.GET['stream'] - except (AttributeError, KeyError): - return json_error("Missing api_key or stream parameter.") - - try: - user_profile = UserProfile.objects.get(api_key=api_key) - request.user = user_profile - except UserProfile.DoesNotExist: - return json_error("Failed to find user with API key: %s" % (api_key,)) - - rate_limit_user(request, user_profile, domain='all') - - payload = xml_fromstring(request.body) - - def get_text(attrs): - start = payload - try: - for attr in attrs: - start = start.find(attr) - return start.text - except AttributeError: - return "" - - try: - event_type = payload.find('event_type').text - description = payload.find('description').text - project_id = payload.find('project_id').text - story_id = get_text(['stories', 'story', 'id']) - # Ugh, the URL in the XML data is not a clickable url that works for the user - # so we try to build one that the user can actually click on - url = "https://www.pivotaltracker.com/s/projects/%s/stories/%s" % (project_id, story_id) - - # Pivotal doesn't tell us the name of the story, but it's usually in the - # description in quotes as the first quoted string - name_re = re.compile(r'[^"]+"([^"]+)".*') - match = name_re.match(description) - if match and len(match.groups()): - name = match.group(1) - else: - name = "Story changed" # Failed for an unknown reason, show something - more_info = " [(view)](%s)" % (url,) - - if event_type == 'story_update': - subject = name - content = description + more_info - elif event_type == 'note_create': - subject = "Comment added" - content = description + more_info - elif event_type == 'story_create': - issue_desc = get_text(['stories', 'story', 'description']) - issue_type = get_text(['stories', 'story', 'story_type']) - issue_status = get_text(['stories', 'story', 'current_state']) - estimate = get_text(['stories', 'story', 'estimate']) - if estimate != '': - estimate = " worth %s story points" % (estimate,) - subject = name - content = "%s (%s %s%s):\n\n> %s\n\n%s" % (description, - issue_status, - issue_type, - estimate, - issue_desc, - more_info) - - except AttributeError: - return json_error("Failed to extract data from Pivotal XML response") - - subject = elide_subject(subject) - - check_send_message(user_profile, get_client("API"), "stream", [stream], subject, content) - return json_success() - -# Beanstalk's web hook UI rejects url with a @ in the username section of a url -# So we ask the user to replace them with %40 -# We manually fix the username here before passing it along to @authenticated_rest_api_view -def beanstalk_decoder(view_func): - @wraps(view_func) - def _wrapped_view_func(request, *args, **kwargs): - try: - auth_type, encoded_value = request.META['HTTP_AUTHORIZATION'].split() - if auth_type.lower() == "basic": - email, api_key = base64.b64decode(encoded_value).split(":") - email = email.replace('%40', '@') - request.META['HTTP_AUTHORIZATION'] = "Basic %s" % (base64.b64encode("%s:%s" % (email, api_key))) - except: - pass - - return view_func(request, *args, **kwargs) - - return _wrapped_view_func - -@beanstalk_decoder -@authenticated_rest_api_view -@has_request_variables -def api_beanstalk_webhook(request, user_profile, - payload=REQ(converter=json_to_dict)): - # Beanstalk supports both SVN and git repositories - # We distinguish between the two by checking for a - # 'uri' key that is only present for git repos - git_repo = 'uri' in payload - if git_repo: - # To get a linkable url, - subject, content = build_message_from_gitlog(user_profile, payload['repository']['name'], - payload['ref'], payload['commits'], - payload['before'], payload['after'], - payload['repository']['url'], - payload['pusher_name']) - else: - author = payload.get('author_full_name') - url = payload.get('changeset_url') - revision = payload.get('revision') - (short_commit_msg, _, _) = payload.get('message').partition("\n") - - subject = "svn r%s" % (revision,) - content = "%s pushed [revision %s](%s):\n\n> %s" % (author, revision, url, short_commit_msg) - - subject = elide_subject(subject) - - check_send_message(user_profile, get_client("API"), "stream", ["commits"], subject, content) - return json_success() - -@csrf_exempt -@has_request_variables -def api_newrelic_webhook(request, alert=REQ(converter=json_to_dict, default=None), - deployment=REQ(converter=json_to_dict, default=None)): - try: - api_key = request.GET['api_key'] - stream = request.GET['stream'] - except (AttributeError, KeyError): - return json_error("Missing api_key or stream parameter.") - - try: - user_profile = UserProfile.objects.get(api_key=api_key) - request.user = user_profile - except UserProfile.DoesNotExist: - return json_error("Failed to find user with API key: %s" % (api_key,)) - - rate_limit_user(request, user_profile, domain='all') - - if alert: - # Use the message as the subject because it stays the same for - # "opened", "acknowledged", and "closed" messages that should be - # grouped. - subject = alert['message'] - content = "%(long_description)s\n[View alert](%(alert_url)s)" % (alert) - elif deployment: - subject = "%s deploy" % (deployment['application_name']) - content = """`%(revision)s` deployed by **%(deployed_by)s** -%(description)s - -%(changelog)s""" % (deployment) - else: - return json_error("Unknown webhook request") - - subject = elide_subject(subject) - check_send_message(user_profile, get_client("API"), "stream", [stream], subject, content) - return json_success() - def get_status_list(requesting_user_profile): return {'presences': get_status_dict(requesting_user_profile), 'server_timestamp': time.time()} -@authenticated_rest_api_view -@has_request_variables -def api_bitbucket_webhook(request, user_profile, payload=REQ(converter=json_to_dict), - stream=REQ(default='commits')): - repository = payload['repository'] - commits = [{'id': commit['raw_node'], 'message': commit['message'], - 'url': '%s%scommits/%s' % (payload['canon_url'], - repository['absolute_url'], - commit['raw_node'])} - for commit in payload['commits']] - - subject = repository['name'] - if len(commits) == 0: - # Bitbucket doesn't give us enough information to really give - # a useful message :/ - content = ("%s [force pushed](%s)" - % (payload['user'], - payload['canon_url'] + repository['absolute_url'])) - else: - content = build_commit_list_content(commits, payload['commits'][-1]['branch'], - None, payload['user']) - - subject = elide_subject(subject) - check_send_message(user_profile, get_client("API"), "stream", [stream], subject, content) - return json_success() - @authenticated_json_post_view @has_request_variables def json_update_active_status(request, user_profile, status=REQ): diff --git a/zerver/views/webhooks.py b/zerver/views/webhooks.py new file mode 100644 index 0000000000..4b26ee51aa --- /dev/null +++ b/zerver/views/webhooks.py @@ -0,0 +1,423 @@ +# Webhooks for external integrations. + +from __future__ import absolute_import + +from django.conf import settings +from django.views.decorators.csrf import csrf_exempt +from zerver.models import UserProfile, get_client, MAX_SUBJECT_LENGTH +from zerver.lib.actions import check_send_message +from zerver.lib.response import json_success, json_error +from zerver.decorator import rate_limit_user, authenticated_api_view, REQ, \ + has_request_variables, json_to_dict, authenticated_rest_api_view +from zerver.views import send_message_backend + +from defusedxml.ElementTree import fromstring as xml_fromstring + +import base64 +import logging +import re +import ujson +from functools import wraps + +@authenticated_api_view +@has_request_variables +def api_github_landing(request, user_profile, event=REQ, + payload=REQ(converter=json_to_dict), + branches=REQ(default=''), + stream=REQ(default='')): + # TODO: this should all be moved to an external bot + repository = payload['repository'] + + # Special hook for capturing event data + try: + if repository['name'] == 'zulip-test': + with open('/var/log/humbug/github-payloads', 'a') as f: + f.write(ujson.dumps({'event': event, 'payload': payload})) + f.write("\n") + except Exception: + logging.exception("Error while capturing Github event") + + if not stream: + stream = 'commits' + + # CUSTOMER18 has requested not to get pull request notifications + if event == 'pull_request' and user_profile.realm.domain not in ['customer18.invalid', 'zulip.com']: + pull_req = payload['pull_request'] + + subject = "%s: pull request %d" % (repository['name'], + pull_req['number']) + content = ("Pull request from %s [%s](%s):\n\n %s\n\n> %s" + % (pull_req['user']['login'], + payload['action'], + pull_req['html_url'], + pull_req['title'], + pull_req['body'])) + elif event == 'push': + short_ref = re.sub(r'^refs/heads/', '', payload['ref']) + # This is a bit hackish, but is basically so that CUSTOMER18 doesn't + # get spammed when people commit to non-master all over the place. + # Long-term, this will be replaced by some GitHub configuration + # option of which branches to notify on. + if short_ref != 'master' and user_profile.realm.domain in ['customer18.invalid', 'zulip.com']: + return json_success() + + if branches: + # If we are given a whitelist of branches, then we silently ignore + # any push notification on a branch that is not in our whitelist. + if short_ref not in re.split('[\s,;|]+', branches): + return json_success() + + + subject, content = build_message_from_gitlog(user_profile, repository['name'], + payload['ref'], payload['commits'], + payload['before'], payload['after'], + payload['compare'], + payload['pusher']['name']) + elif event == 'issues': + if user_profile.realm.domain not in ('zulip.com', 'customer5.invalid'): + return json_success() + + issue = payload['issue'] + subject = "%s: issue %d %s" % (repository['name'], issue['number'], payload['action']) + content = ("%s %s [issue %d](%s): %s\n\n> %s" + % (issue['user']['login'], + payload['action'], + issue['number'], + issue['html_url'], + issue['title'], + issue['body'])) + stream = 'issues' + else: + # We don't handle other events even though we get notified + # about them + return json_success() + + subject = elide_subject(subject) + + request.client = get_client("github_bot") + return send_message_backend(request, user_profile, + message_type_name="stream", + message_to=[stream], + forged=False, subject_name=subject, + message_content=content) + +def build_commit_list_content(commits, branch, compare_url, pusher): + if compare_url is not None: + push_text = "[pushed](%s)" % (compare_url,) + else: + push_text = "pushed" + content = ("%s %s to branch %s\n\n" + % (pusher, + push_text, + branch)) + num_commits = len(commits) + max_commits = 10 + truncated_commits = commits[:max_commits] + for commit in truncated_commits: + short_id = commit['id'][:7] + (short_commit_msg, _, _) = commit['message'].partition("\n") + content += "* [%s](%s): %s\n" % (short_id, commit['url'], + short_commit_msg) + if (num_commits > max_commits): + content += ("\n[and %d more commits]" + % (num_commits - max_commits,)) + + return content + +def build_message_from_gitlog(user_profile, name, ref, commits, before, after, url, pusher): + short_ref = re.sub(r'^refs/heads/', '', ref) + subject = name + + if re.match(r'^0+$', after): + content = "%s deleted branch %s" % (pusher, + short_ref) + elif len(commits) == 0: + content = ("%s [force pushed](%s) to branch %s. Head is now %s" + % (pusher, + url, + short_ref, + after[:7])) + else: + content = build_commit_list_content(commits, short_ref, url, pusher) + + return (subject, content) + +def elide_subject(subject): + if len(subject) > MAX_SUBJECT_LENGTH: + subject = subject[:57].rstrip() + '...' + return subject + +@csrf_exempt +def api_jira_webhook(request): + try: + api_key = request.GET['api_key'] + except (AttributeError, KeyError): + return json_error("Missing api_key parameter.") + + try: + payload = ujson.loads(request.body) + except ValueError: + return json_error("Malformed JSON input") + + try: + stream = request.GET['stream'] + except (AttributeError, KeyError): + stream = 'jira' + + try: + user_profile = UserProfile.objects.get(api_key=api_key) + request.user = user_profile + except UserProfile.DoesNotExist: + return json_error("Failed to find user with API key: %s" % (api_key,)) + + rate_limit_user(request, user_profile, domain='all') + + def get_in(payload, keys, default=''): + try: + for key in keys: + payload = payload[key] + except (AttributeError, KeyError): + return default + return payload + + event = payload.get('webhookEvent') + author = get_in(payload, ['user', 'displayName']) + issueId = get_in(payload, ['issue', 'key']) + # Guess the URL as it is not specified in the payload + # We assume that there is a /browse/BUG-### page + # from the REST url of the issue itself + baseUrl = re.match("(.*)\/rest\/api/.*", get_in(payload, ['issue', 'self'])) + if baseUrl and len(baseUrl.groups()): + issue = "[%s](%s/browse/%s)" % (issueId, baseUrl.group(1), issueId) + else: + issue = issueId + title = get_in(payload, ['issue', 'fields', 'summary']) + priority = get_in(payload, ['issue', 'fields', 'priority', 'name']) + assignee = get_in(payload, ['assignee', 'displayName'], 'no one') + subject = "%s: %s" % (issueId, title) + + if event == 'jira:issue_created': + content = "%s **created** %s priority %s, assigned to **%s**:\n\n> %s" % \ + (author, issue, priority, assignee, title) + elif event == 'jira:issue_deleted': + content = "%s **deleted** %s!" % \ + (author, issue) + elif event == 'jira:issue_updated': + # Reassigned, commented, reopened, and resolved events are all bundled + # into this one 'updated' event type, so we try to extract the meaningful + # event that happened + content = "%s **updated** %s:\n\n" % (author, issue) + changelog = get_in(payload, ['changelog',]) + comment = get_in(payload, ['comment', 'body']) + + if changelog != '': + # Use the changelog to display the changes, whitelist types we accept + items = changelog.get('items') + for item in items: + field = item.get('field') + if field in ('status', 'assignee'): + content += "* Changed %s from **%s** to **%s**\n" % (field, item.get('fromString'), item.get('toString')) + + if comment != '': + content += "\n> %s" % (comment,) + elif 'transition' in payload: + from_status = get_in(payload, ['transition', 'from_status']) + to_status = get_in(payload, ['transition', 'to_status']) + content = "%s **transitioned** %s from %s to %s" % (author, issue, from_status, to_status) + else: + # Unknown event type + if not settings.TEST_SUITE: + logging.warning("Got JIRA event type we don't understand: %s" % (event,)) + return json_error("Unknown JIRA event type") + + subject = elide_subject(subject) + + check_send_message(user_profile, get_client("API"), "stream", [stream], subject, content) + return json_success() + +@csrf_exempt +def api_pivotal_webhook(request): + try: + api_key = request.GET['api_key'] + stream = request.GET['stream'] + except (AttributeError, KeyError): + return json_error("Missing api_key or stream parameter.") + + try: + user_profile = UserProfile.objects.get(api_key=api_key) + request.user = user_profile + except UserProfile.DoesNotExist: + return json_error("Failed to find user with API key: %s" % (api_key,)) + + rate_limit_user(request, user_profile, domain='all') + + payload = xml_fromstring(request.body) + + def get_text(attrs): + start = payload + try: + for attr in attrs: + start = start.find(attr) + return start.text + except AttributeError: + return "" + + try: + event_type = payload.find('event_type').text + description = payload.find('description').text + project_id = payload.find('project_id').text + story_id = get_text(['stories', 'story', 'id']) + # Ugh, the URL in the XML data is not a clickable url that works for the user + # so we try to build one that the user can actually click on + url = "https://www.pivotaltracker.com/s/projects/%s/stories/%s" % (project_id, story_id) + + # Pivotal doesn't tell us the name of the story, but it's usually in the + # description in quotes as the first quoted string + name_re = re.compile(r'[^"]+"([^"]+)".*') + match = name_re.match(description) + if match and len(match.groups()): + name = match.group(1) + else: + name = "Story changed" # Failed for an unknown reason, show something + more_info = " [(view)](%s)" % (url,) + + if event_type == 'story_update': + subject = name + content = description + more_info + elif event_type == 'note_create': + subject = "Comment added" + content = description + more_info + elif event_type == 'story_create': + issue_desc = get_text(['stories', 'story', 'description']) + issue_type = get_text(['stories', 'story', 'story_type']) + issue_status = get_text(['stories', 'story', 'current_state']) + estimate = get_text(['stories', 'story', 'estimate']) + if estimate != '': + estimate = " worth %s story points" % (estimate,) + subject = name + content = "%s (%s %s%s):\n\n> %s\n\n%s" % (description, + issue_status, + issue_type, + estimate, + issue_desc, + more_info) + + except AttributeError: + return json_error("Failed to extract data from Pivotal XML response") + + subject = elide_subject(subject) + + check_send_message(user_profile, get_client("API"), "stream", [stream], subject, content) + return json_success() + +# Beanstalk's web hook UI rejects url with a @ in the username section of a url +# So we ask the user to replace them with %40 +# We manually fix the username here before passing it along to @authenticated_rest_api_view +def beanstalk_decoder(view_func): + @wraps(view_func) + def _wrapped_view_func(request, *args, **kwargs): + try: + auth_type, encoded_value = request.META['HTTP_AUTHORIZATION'].split() + if auth_type.lower() == "basic": + email, api_key = base64.b64decode(encoded_value).split(":") + email = email.replace('%40', '@') + request.META['HTTP_AUTHORIZATION'] = "Basic %s" % (base64.b64encode("%s:%s" % (email, api_key))) + except: + pass + + return view_func(request, *args, **kwargs) + + return _wrapped_view_func + +@beanstalk_decoder +@authenticated_rest_api_view +@has_request_variables +def api_beanstalk_webhook(request, user_profile, + payload=REQ(converter=json_to_dict)): + # Beanstalk supports both SVN and git repositories + # We distinguish between the two by checking for a + # 'uri' key that is only present for git repos + git_repo = 'uri' in payload + if git_repo: + # To get a linkable url, + subject, content = build_message_from_gitlog(user_profile, payload['repository']['name'], + payload['ref'], payload['commits'], + payload['before'], payload['after'], + payload['repository']['url'], + payload['pusher_name']) + else: + author = payload.get('author_full_name') + url = payload.get('changeset_url') + revision = payload.get('revision') + (short_commit_msg, _, _) = payload.get('message').partition("\n") + + subject = "svn r%s" % (revision,) + content = "%s pushed [revision %s](%s):\n\n> %s" % (author, revision, url, short_commit_msg) + + subject = elide_subject(subject) + + check_send_message(user_profile, get_client("API"), "stream", ["commits"], subject, content) + return json_success() + +@csrf_exempt +@has_request_variables +def api_newrelic_webhook(request, alert=REQ(converter=json_to_dict, default=None), + deployment=REQ(converter=json_to_dict, default=None)): + try: + api_key = request.GET['api_key'] + stream = request.GET['stream'] + except (AttributeError, KeyError): + return json_error("Missing api_key or stream parameter.") + + try: + user_profile = UserProfile.objects.get(api_key=api_key) + request.user = user_profile + except UserProfile.DoesNotExist: + return json_error("Failed to find user with API key: %s" % (api_key,)) + + rate_limit_user(request, user_profile, domain='all') + + if alert: + # Use the message as the subject because it stays the same for + # "opened", "acknowledged", and "closed" messages that should be + # grouped. + subject = alert['message'] + content = "%(long_description)s\n[View alert](%(alert_url)s)" % (alert) + elif deployment: + subject = "%s deploy" % (deployment['application_name']) + content = """`%(revision)s` deployed by **%(deployed_by)s** +%(description)s + +%(changelog)s""" % (deployment) + else: + return json_error("Unknown webhook request") + + subject = elide_subject(subject) + check_send_message(user_profile, get_client("API"), "stream", [stream], subject, content) + return json_success() + +@authenticated_rest_api_view +@has_request_variables +def api_bitbucket_webhook(request, user_profile, payload=REQ(converter=json_to_dict), + stream=REQ(default='commits')): + repository = payload['repository'] + commits = [{'id': commit['raw_node'], 'message': commit['message'], + 'url': '%s%scommits/%s' % (payload['canon_url'], + repository['absolute_url'], + commit['raw_node'])} + for commit in payload['commits']] + + subject = repository['name'] + if len(commits) == 0: + # Bitbucket doesn't give us enough information to really give + # a useful message :/ + content = ("%s [force pushed](%s)" + % (payload['user'], + payload['canon_url'] + repository['absolute_url'])) + else: + content = build_commit_list_content(commits, payload['commits'][-1]['branch'], + None, payload['user']) + + subject = elide_subject(subject) + check_send_message(user_profile, get_client("API"), "stream", [stream], subject, content) + return json_success() diff --git a/zproject/urls.py b/zproject/urls.py index 83cc88dae6..18ea92c9d4 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -139,12 +139,12 @@ urlpatterns += patterns('zerver.views', url(r'^api/v1/fetch_api_key$', 'api_fetch_api_key'), # These are integration-specific web hook callbacks - url(r'^api/v1/external/beanstalk$' , 'api_beanstalk_webhook'), - url(r'^api/v1/external/github$', 'api_github_landing'), - url(r'^api/v1/external/jira$', 'api_jira_webhook'), - url(r'^api/v1/external/pivotal$', 'api_pivotal_webhook'), - url(r'^api/v1/external/newrelic$', 'api_newrelic_webhook'), - url(r'^api/v1/external/bitbucket$', 'api_bitbucket_webhook'), + url(r'^api/v1/external/beanstalk$' , 'webhooks.api_beanstalk_webhook'), + url(r'^api/v1/external/github$', 'webhooks.api_github_landing'), + url(r'^api/v1/external/jira$', 'webhooks.api_jira_webhook'), + url(r'^api/v1/external/pivotal$', 'webhooks.api_pivotal_webhook'), + url(r'^api/v1/external/newrelic$', 'webhooks.api_newrelic_webhook'), + url(r'^api/v1/external/bitbucket$', 'webhooks.api_bitbucket_webhook'), ) v1_api_and_json_patterns = patterns('zerver.views',