zulip/zerver/views/webhooks/github.py

308 lines
13 KiB
Python
Raw Normal View History

from __future__ import absolute_import
from django.conf import settings
from zerver.models import get_client, UserProfile
from zerver.lib.response import json_success
from zerver.lib.validator import check_dict
from zerver.decorator import authenticated_api_view, REQ, has_request_variables, to_non_negative_int, flexible_boolean
from zerver.views.messages import send_message_backend
from zerver.lib.webhooks.git import get_push_commits_event_message,\
SUBJECT_WITH_BRANCH_TEMPLATE, get_force_push_commits_event_message, \
get_remove_branch_event_message, get_pull_request_event_message,\
get_issue_event_message, SUBJECT_WITH_PR_OR_ISSUE_INFO_TEMPLATE,\
get_commits_comment_action_message
import logging
import re
import ujson
from six import text_type
from typing import Any, Mapping, Optional, Sequence, Tuple
from zerver.lib.str_utils import force_str
from django.http import HttpRequest, HttpResponse
ZULIP_TEST_REPO_NAME = 'zulip-test'
ZULIP_TEST_REPO_ID = 6893087
def is_test_repository(repository):
# type: (Mapping[text_type, Any]) -> bool
return repository['name'] == ZULIP_TEST_REPO_NAME and repository['id'] == ZULIP_TEST_REPO_ID
class UnknownEventType(Exception):
pass
def github_pull_request_content(payload):
# type: (Mapping[text_type, Any]) -> text_type
pull_request = payload['pull_request']
action = get_pull_request_or_issue_action(payload)
if action in ('opened', 'edited'):
return get_pull_request_event_message(
payload['sender']['login'],
action,
pull_request['html_url'],
pull_request['number'],
pull_request['head']['ref'],
pull_request['base']['ref'],
pull_request['body'],
get_pull_request_or_issue_assignee(pull_request)
)
return get_pull_request_event_message(
payload['sender']['login'],
action,
pull_request['html_url'],
pull_request['number']
)
def github_issues_content(payload):
# type: (Mapping[text_type, Any]) -> text_type
issue = payload['issue']
action = get_pull_request_or_issue_action(payload)
if action in ('opened', 'edited'):
return get_issue_event_message(
payload['sender']['login'],
action,
issue['html_url'],
issue['number'],
issue['body'],
get_pull_request_or_issue_assignee(issue)
)
return get_issue_event_message(
payload['sender']['login'],
action,
issue['html_url'],
issue['number'],
)
def github_object_commented_content(payload, type):
# type: (Mapping[text_type, Any], text_type) -> text_type
comment = payload['comment']
issue = payload['issue']
action = u'[commented]({}) on'.format(comment['html_url'])
return get_pull_request_event_message(
comment['user']['login'],
action,
issue['html_url'],
issue['number'],
message=comment['body'],
type=type
)
def get_pull_request_or_issue_action(payload):
# type: (Mapping[text_type, Any]) -> text_type
return 'synchronized' if payload['action'] == 'synchronize' else payload['action']
def get_pull_request_or_issue_assignee(object_payload):
# type: (Mapping[text_type, Any]) -> text_type
assignee_dict = object_payload.get('assignee')
if assignee_dict:
return assignee_dict.get('login')
def get_pull_request_or_issue_subject(repository, payload_object, type):
# type: (Mapping[text_type, Any], Mapping[text_type, Any], text_type) -> text_type
return SUBJECT_WITH_PR_OR_ISSUE_INFO_TEMPLATE.format(
repo=repository['name'],
type=type,
id=payload_object['number'],
title=payload_object['title']
)
def github_generic_subject(noun, topic_focus, blob):
# type: (text_type, text_type, Mapping[text_type, Any]) -> text_type
# issue and pull_request objects have the same fields we're interested in
return u'%s: %s %d: %s' % (topic_focus, noun, blob['number'], blob['title'])
def api_github_v1(user_profile, event, payload, branches, stream, **kwargs):
# type: (UserProfile, text_type, Mapping[text_type, Any], text_type, text_type, **Any) -> Tuple[text_type, text_type, text_type]
"""
processes github payload with version 1 field specification
`payload` comes in unmodified from github
`stream` is set to 'commits' if otherwise unset
"""
commit_stream = stream
issue_stream = 'issues'
return api_github_v2(user_profile, event, payload, branches, stream, commit_stream, issue_stream, **kwargs)
def api_github_v2(user_profile, event, payload, branches, default_stream,
commit_stream, issue_stream, topic_focus = None):
# type: (UserProfile, text_type, Mapping[text_type, Any], text_type, text_type, text_type, text_type, Optional[text_type]) -> Tuple[text_type, text_type, text_type]
"""
processes github payload with version 2 field specification
`payload` comes in unmodified from github
`default_stream` is set to what `stream` is in v1 above
`commit_stream` and `issue_stream` fall back to `default_stream` if they are empty
This and allowing alternative endpoints is what distinguishes v1 from v2 of the github configuration
"""
target_stream = commit_stream if commit_stream else default_stream
issue_stream = issue_stream if issue_stream else default_stream
repository = payload['repository']
topic_focus = topic_focus if topic_focus else repository['name']
# Event Handlers
if event == 'pull_request':
subject = get_pull_request_or_issue_subject(repository, payload['pull_request'], 'PR')
content = github_pull_request_content(payload)
elif event == 'issues':
# in v1, we assume that this stream exists since it is
# deprecated and the few realms that use it already have the
# stream
target_stream = issue_stream
subject = get_pull_request_or_issue_subject(repository, payload['issue'], 'Issue')
content = github_issues_content(payload)
elif event == 'issue_comment':
# Comments on both issues and pull requests come in as issue_comment events
issue = payload['issue']
if 'pull_request' not in issue or issue['pull_request']['diff_url'] is None:
# It's an issues comment
target_stream = issue_stream
type = 'Issue'
subject = get_pull_request_or_issue_subject(repository, payload['issue'], type)
else:
# It's a pull request comment
type = 'PR'
subject = get_pull_request_or_issue_subject(repository, payload['issue'], type)
content = github_object_commented_content(payload, type)
elif event == 'push':
subject, content = build_message_from_gitlog(user_profile, topic_focus,
payload['ref'], payload['commits'],
payload['before'], payload['after'],
payload['compare'],
payload['pusher']['name'],
forced=payload['forced'],
created=payload['created'])
elif event == 'commit_comment':
subject = topic_focus
comment = payload.get('comment')
action = u'[commented]({})'.format(comment['html_url'])
content = get_commits_comment_action_message(
comment['user']['login'],
action,
comment['html_url'].split('#', 1)[0],
comment['commit_id'],
comment['body'],
)
else:
raise UnknownEventType(force_str(u'Event %s is unknown and cannot be handled' % (event,)))
return target_stream, subject, content
@authenticated_api_view(is_webhook=True)
@has_request_variables
def api_github_landing(request, user_profile, event=REQ(),
payload=REQ(validator=check_dict([])),
branches=REQ(default=''),
stream=REQ(default=''),
version=REQ(converter=to_non_negative_int, default=1),
commit_stream=REQ(default=''),
issue_stream=REQ(default=''),
exclude_pull_requests=REQ(converter=flexible_boolean, default=False),
exclude_issues=REQ(converter=flexible_boolean, default=False),
exclude_commits=REQ(converter=flexible_boolean, default=False),
emphasize_branch_in_topic=REQ(converter=flexible_boolean, default=False),
):
# type: (HttpRequest, UserProfile, text_type, Mapping[text_type, Any], text_type, text_type, int, text_type, text_type, bool, bool, bool, bool) -> HttpResponse
repository = payload['repository']
# Special hook for capturing event data. If we see our special test repo, log the payload from github.
try:
if is_test_repository(repository) and settings.PRODUCTION:
with open('/var/log/zulip/github-payloads', 'a') as f:
f.write(ujson.dumps({'event': event,
'payload': payload,
'branches': branches,
'stream': stream,
'version': version,
'commit_stream': commit_stream,
'issue_stream': issue_stream,
'exclude_pull_requests': exclude_pull_requests,
'exclude_issues': exclude_issues,
'exclude_commits': exclude_commits,
'emphasize_branch_in_topic': emphasize_branch_in_topic,
}))
f.write('\n')
except Exception:
logging.exception('Error while capturing Github event')
if not stream:
stream = 'commits'
short_ref = re.sub(r'^refs/heads/', '', payload.get('ref', ''))
kwargs = dict()
if emphasize_branch_in_topic and short_ref:
kwargs['topic_focus'] = short_ref
allowed_events = set()
if not exclude_pull_requests:
allowed_events.add('pull_request')
if not exclude_issues:
allowed_events.add('issues')
allowed_events.add('issue_comment')
if not exclude_commits:
allowed_events.add('push')
allowed_events.add('commit_comment')
if event not in allowed_events:
return json_success()
# We filter issue_comment events for issue creation events
if event == 'issue_comment' and payload['action'] != 'created':
return json_success()
if event == 'push':
# 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 branches and short_ref not in re.split('[\s,;|]+', branches):
return json_success()
# Map payload to the handler with the right version
if version == 2:
target_stream, subject, content = api_github_v2(user_profile, event, payload, branches,
stream, commit_stream, issue_stream,
**kwargs)
else:
target_stream, subject, content = api_github_v1(user_profile, event, payload, branches,
stream, **kwargs)
request.client = get_client('ZulipGitHubWebhook')
return send_message_backend(request, user_profile,
message_type_name='stream',
message_to=[target_stream],
forged=False, subject_name=subject,
message_content=content)
def build_message_from_gitlog(user_profile, name, ref, commits, before, after, url, pusher, forced=None, created=None):
# type: (UserProfile, text_type, text_type, List[Dict[str, str]], text_type, text_type, text_type, text_type, Optional[text_type], Optional[text_type]) -> Tuple[text_type, text_type]
short_ref = re.sub(r'^refs/heads/', '', ref)
subject = SUBJECT_WITH_BRANCH_TEMPLATE.format(repo=name, branch=short_ref)
if re.match(r'^0+$', after):
content = get_remove_branch_event_message(pusher, short_ref)
# 'created' and 'forced' are github flags; the second check is for beanstalk
elif (forced and not created) or (forced is None and len(commits) == 0):
content = get_force_push_commits_event_message(pusher, url, short_ref, after[:7])
else:
commits = _transform_commits_list_to_common_format(commits)
content = get_push_commits_event_message(pusher, url, short_ref, commits)
return subject, content
def _transform_commits_list_to_common_format(commits):
# type: (List[Dict[str, str]]) -> List[Dict[str, str]]
new_commits_list = []
for commit in commits:
new_commits_list.append({
'sha': commit.get('id'),
'url': commit.get('url'),
'message': commit.get('message'),
})
return new_commits_list