mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 14:03:30 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			376 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			376 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# Webhooks for external integrations.
 | 
						|
import re
 | 
						|
from functools import partial
 | 
						|
from six.moves import zip
 | 
						|
from typing import Any, Callable, Dict, List, Optional, Text
 | 
						|
from django.http import HttpRequest, HttpResponse
 | 
						|
from django.utils.translation import ugettext as _
 | 
						|
from zerver.lib.actions import check_send_stream_message
 | 
						|
from zerver.lib.response import json_success, json_error
 | 
						|
from zerver.decorator import REQ, has_request_variables, api_key_only_webhook_view
 | 
						|
from zerver.models import UserProfile
 | 
						|
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,\
 | 
						|
    SUBJECT_WITH_PR_OR_ISSUE_INFO_TEMPLATE, get_issue_event_message, get_commits_comment_action_message,\
 | 
						|
    get_push_tag_event_message
 | 
						|
 | 
						|
 | 
						|
BITBUCKET_SUBJECT_TEMPLATE = '{repository_name}'
 | 
						|
USER_PART = 'User {display_name}(login: {username})'
 | 
						|
 | 
						|
BITBUCKET_FORK_BODY = USER_PART + ' forked the repository into [{fork_name}]({fork_url}).'
 | 
						|
BITBUCKET_COMMIT_STATUS_CHANGED_BODY = '[System {key}]({system_url}) changed status of {commit_info} to {status}.'
 | 
						|
 | 
						|
 | 
						|
PULL_REQUEST_SUPPORTED_ACTIONS = [
 | 
						|
    'approved',
 | 
						|
    'unapproved',
 | 
						|
    'created',
 | 
						|
    'updated',
 | 
						|
    'rejected',
 | 
						|
    'fulfilled',
 | 
						|
    'comment_created',
 | 
						|
    'comment_updated',
 | 
						|
    'comment_deleted',
 | 
						|
]
 | 
						|
 | 
						|
class UnknownTriggerType(Exception):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
@api_key_only_webhook_view('Bitbucket2')
 | 
						|
@has_request_variables
 | 
						|
def api_bitbucket2_webhook(request, user_profile, payload=REQ(argument_type='body'),
 | 
						|
                           stream=REQ(default='bitbucket'), branches=REQ(default=None)):
 | 
						|
    # type: (HttpRequest, UserProfile, Dict[str, Any], str, Optional[Text]) -> HttpResponse
 | 
						|
    type = get_type(request, payload)
 | 
						|
    if type != 'push':
 | 
						|
        subject = get_subject_based_on_type(payload, type)
 | 
						|
        body = get_body_based_on_type(type)(payload)
 | 
						|
        check_send_stream_message(user_profile, request.client,
 | 
						|
                                  stream, subject, body)
 | 
						|
    else:
 | 
						|
        branch = get_branch_name_for_push_event(payload)
 | 
						|
        if branch and branches:
 | 
						|
            if branches.find(branch) == -1:
 | 
						|
                return json_success()
 | 
						|
        subjects = get_push_subjects(payload)
 | 
						|
        bodies_list = get_push_bodies(payload)
 | 
						|
        for body, subject in zip(bodies_list, subjects):
 | 
						|
            check_send_stream_message(user_profile, request.client,
 | 
						|
                                      stream, subject, body)
 | 
						|
    return json_success()
 | 
						|
 | 
						|
def get_subject_for_branch_specified_events(payload, branch_name=None):
 | 
						|
    # type: (Dict[str, Any], Optional[Text]) -> Text
 | 
						|
    return SUBJECT_WITH_BRANCH_TEMPLATE.format(
 | 
						|
        repo=get_repository_name(payload['repository']),
 | 
						|
        branch=get_branch_name_for_push_event(payload) if branch_name is None else branch_name
 | 
						|
    )
 | 
						|
 | 
						|
def get_push_subjects(payload):
 | 
						|
    # type: (Dict[str, Any]) -> List[str]
 | 
						|
    subjects_list = []
 | 
						|
    for change in payload['push']['changes']:
 | 
						|
        potential_tag = (change['new'] or change['old'] or {}).get('type')
 | 
						|
        if potential_tag == 'tag':
 | 
						|
            subjects_list.append(str(get_subject(payload)))
 | 
						|
        else:
 | 
						|
            if change.get('new'):
 | 
						|
                branch_name = change['new']['name']
 | 
						|
            else:
 | 
						|
                branch_name = change['old']['name']
 | 
						|
            subjects_list.append(str(get_subject_for_branch_specified_events(payload, branch_name)))
 | 
						|
    return subjects_list
 | 
						|
 | 
						|
def get_subject(payload):
 | 
						|
    # type: (Dict[str, Any]) -> str
 | 
						|
    assert(payload['repository'] is not None)
 | 
						|
    return BITBUCKET_SUBJECT_TEMPLATE.format(repository_name=get_repository_name(payload['repository']))
 | 
						|
 | 
						|
def get_subject_based_on_type(payload, type):
 | 
						|
    # type: (Dict[str, Any], str) -> Text
 | 
						|
    if type.startswith('pull_request'):
 | 
						|
        return SUBJECT_WITH_PR_OR_ISSUE_INFO_TEMPLATE.format(
 | 
						|
            repo=get_repository_name(payload['repository']),
 | 
						|
            type='PR',
 | 
						|
            id=payload['pullrequest']['id'],
 | 
						|
            title=payload['pullrequest']['title']
 | 
						|
        )
 | 
						|
    if type.startswith('issue'):
 | 
						|
        return SUBJECT_WITH_PR_OR_ISSUE_INFO_TEMPLATE.format(
 | 
						|
            repo=get_repository_name(payload['repository']),
 | 
						|
            type='Issue',
 | 
						|
            id=payload['issue']['id'],
 | 
						|
            title=payload['issue']['title']
 | 
						|
        )
 | 
						|
    return get_subject(payload)
 | 
						|
 | 
						|
def get_type(request, payload):
 | 
						|
    # type: (HttpRequest, Dict[str, Any]) -> str
 | 
						|
    event_key = request.META.get("HTTP_X_EVENT_KEY")
 | 
						|
    if payload.get('push'):
 | 
						|
        return 'push'
 | 
						|
    elif payload.get('fork'):
 | 
						|
        return 'fork'
 | 
						|
    elif payload.get('comment') and payload.get('commit'):
 | 
						|
        return 'commit_comment'
 | 
						|
    elif payload.get('commit_status'):
 | 
						|
        return 'change_commit_status'
 | 
						|
    elif payload.get('issue'):
 | 
						|
        if payload.get('changes'):
 | 
						|
            return "issue_updated"
 | 
						|
        if payload.get('comment'):
 | 
						|
            return 'issue_commented'
 | 
						|
        return "issue_created"
 | 
						|
    elif payload.get('pullrequest'):
 | 
						|
        pull_request_template = 'pull_request_{}'
 | 
						|
        action = re.match('pullrequest:(?P<action>.*)$', event_key)
 | 
						|
        if action:
 | 
						|
            action = action.group('action')
 | 
						|
            if action in PULL_REQUEST_SUPPORTED_ACTIONS:
 | 
						|
                return pull_request_template.format(action)
 | 
						|
    raise UnknownTriggerType("We don't support {} event type".format(event_key))
 | 
						|
 | 
						|
def get_body_based_on_type(type):
 | 
						|
    # type: (str) -> Callable[[Dict[str, Any]], Text]
 | 
						|
    fn = GET_SINGLE_MESSAGE_BODY_DEPENDING_ON_TYPE_MAPPER.get(type)
 | 
						|
    assert callable(fn)  # type parameter should be pre-checked, so not None
 | 
						|
    return fn
 | 
						|
 | 
						|
def get_push_bodies(payload):
 | 
						|
    # type: (Dict[str, Any]) -> List[Text]
 | 
						|
    messages_list = []
 | 
						|
    for change in payload['push']['changes']:
 | 
						|
        potential_tag = (change['new'] or change['old'] or {}).get('type')
 | 
						|
        if potential_tag == 'tag':
 | 
						|
            messages_list.append(get_push_tag_body(payload, change))
 | 
						|
        elif change.get('closed'):
 | 
						|
            messages_list.append(get_remove_branch_push_body(payload, change))
 | 
						|
        elif change.get('forced'):
 | 
						|
            messages_list.append(get_force_push_body(payload, change))
 | 
						|
        else:
 | 
						|
            messages_list.append(get_normal_push_body(payload, change))
 | 
						|
    return messages_list
 | 
						|
 | 
						|
def get_remove_branch_push_body(payload, change):
 | 
						|
    # type: (Dict[str, Any], Dict[str, Any]) -> Text
 | 
						|
    return get_remove_branch_event_message(
 | 
						|
        get_user_username(payload),
 | 
						|
        change['old']['name'],
 | 
						|
    )
 | 
						|
 | 
						|
def get_force_push_body(payload, change):
 | 
						|
    # type: (Dict[str, Any], Dict[str, Any]) -> Text
 | 
						|
    return get_force_push_commits_event_message(
 | 
						|
        get_user_username(payload),
 | 
						|
        change['links']['html']['href'],
 | 
						|
        change['new']['name'],
 | 
						|
        change['new']['target']['hash']
 | 
						|
    )
 | 
						|
 | 
						|
def get_commit_author_name(commit):
 | 
						|
    # type: (Dict[str, Any]) -> Text
 | 
						|
    if commit['author'].get('user'):
 | 
						|
        return commit['author']['user'].get('username')
 | 
						|
    return commit['author']['raw'].split()[0]
 | 
						|
 | 
						|
def get_normal_push_body(payload, change):
 | 
						|
    # type: (Dict[str, Any], Dict[str, Any]) -> Text
 | 
						|
    commits_data = [{
 | 
						|
        'name': get_commit_author_name(commit),
 | 
						|
        'sha': commit.get('hash'),
 | 
						|
        'url': commit.get('links').get('html').get('href'),
 | 
						|
        'message': commit.get('message'),
 | 
						|
    } for commit in change['commits']]
 | 
						|
 | 
						|
    return get_push_commits_event_message(
 | 
						|
        get_user_username(payload),
 | 
						|
        change['links']['html']['href'],
 | 
						|
        change['new']['name'],
 | 
						|
        commits_data,
 | 
						|
        is_truncated=change['truncated']
 | 
						|
    )
 | 
						|
 | 
						|
def get_fork_body(payload):
 | 
						|
    # type: (Dict[str, Any]) -> str
 | 
						|
    return BITBUCKET_FORK_BODY.format(
 | 
						|
        display_name=get_user_display_name(payload),
 | 
						|
        username=get_user_username(payload),
 | 
						|
        fork_name=get_repository_full_name(payload['fork']),
 | 
						|
        fork_url=get_repository_url(payload['fork'])
 | 
						|
    )
 | 
						|
 | 
						|
def get_commit_comment_body(payload):
 | 
						|
    # type: (Dict[str, Any]) -> Text
 | 
						|
    comment = payload['comment']
 | 
						|
    action = u'[commented]({})'.format(comment['links']['html']['href'])
 | 
						|
    return get_commits_comment_action_message(
 | 
						|
        get_user_username(payload),
 | 
						|
        action,
 | 
						|
        comment['commit']['links']['html']['href'],
 | 
						|
        comment['commit']['hash'],
 | 
						|
        comment['content']['raw'],
 | 
						|
    )
 | 
						|
 | 
						|
def get_commit_status_changed_body(payload):
 | 
						|
    # type: (Dict[str, Any]) -> str
 | 
						|
    commit_id = re.match('.*/commit/(?P<commit_id>[A-Za-z0-9]*$)', payload['commit_status']['links']['commit']['href'])
 | 
						|
    if commit_id:
 | 
						|
        commit_info = "{}/{}".format(get_repository_url(payload['repository']), commit_id.group('commit_id'))
 | 
						|
    else:
 | 
						|
        commit_info = 'commit'
 | 
						|
 | 
						|
    return BITBUCKET_COMMIT_STATUS_CHANGED_BODY.format(
 | 
						|
        key=payload['commit_status']['key'],
 | 
						|
        system_url=payload['commit_status']['url'],
 | 
						|
        commit_info=commit_info,
 | 
						|
        status=payload['commit_status']['state']
 | 
						|
    )
 | 
						|
 | 
						|
def get_issue_commented_body(payload):
 | 
						|
    # type: (Dict[str, Any]) -> Text
 | 
						|
    action = '[commented]({}) on'.format(payload['comment']['links']['html']['href'])
 | 
						|
    return get_issue_action_body(payload, action)
 | 
						|
 | 
						|
def get_issue_action_body(payload, action):
 | 
						|
    # type: (Dict[str, Any], str) -> Text
 | 
						|
    issue = payload['issue']
 | 
						|
    assignee = None
 | 
						|
    message = None
 | 
						|
    if action == 'created':
 | 
						|
        if issue['assignee']:
 | 
						|
            assignee = issue['assignee'].get('username')
 | 
						|
        message = issue['content']['raw']
 | 
						|
 | 
						|
    return get_issue_event_message(
 | 
						|
        get_user_username(payload),
 | 
						|
        action,
 | 
						|
        issue['links']['html']['href'],
 | 
						|
        issue['id'],
 | 
						|
        message,
 | 
						|
        assignee
 | 
						|
    )
 | 
						|
 | 
						|
def get_pull_request_action_body(payload, action):
 | 
						|
    # type: (Dict[str, Any], str) -> Text
 | 
						|
    pull_request = payload['pullrequest']
 | 
						|
    return get_pull_request_event_message(
 | 
						|
        get_user_username(payload),
 | 
						|
        action,
 | 
						|
        get_pull_request_url(pull_request),
 | 
						|
        pull_request.get('id')
 | 
						|
    )
 | 
						|
 | 
						|
def get_pull_request_created_or_updated_body(payload, action):
 | 
						|
    # type: (Dict[str, Any], str) -> Text
 | 
						|
    pull_request = payload['pullrequest']
 | 
						|
    assignee = None
 | 
						|
    if pull_request.get('reviewers'):
 | 
						|
        assignee = pull_request.get('reviewers')[0]['username']
 | 
						|
 | 
						|
    return get_pull_request_event_message(
 | 
						|
        get_user_username(payload),
 | 
						|
        action,
 | 
						|
        get_pull_request_url(pull_request),
 | 
						|
        pull_request.get('id'),
 | 
						|
        target_branch=pull_request['source']['branch']['name'],
 | 
						|
        base_branch=pull_request['destination']['branch']['name'],
 | 
						|
        message=pull_request['description'],
 | 
						|
        assignee=assignee
 | 
						|
    )
 | 
						|
 | 
						|
def get_pull_request_comment_created_action_body(payload):
 | 
						|
    # type: (Dict[str, Any]) -> Text
 | 
						|
    action = '[commented]({})'.format(payload['comment']['links']['html']['href'])
 | 
						|
    return get_pull_request_comment_action_body(payload, action)
 | 
						|
 | 
						|
def get_pull_request_deleted_or_updated_comment_action_body(payload, action):
 | 
						|
    # type: (Dict[str, Any], Text) -> Text
 | 
						|
    action = "{} a [comment]({})".format(action, payload['comment']['links']['html']['href'])
 | 
						|
    return get_pull_request_comment_action_body(payload, action)
 | 
						|
 | 
						|
def get_pull_request_comment_action_body(payload, action):
 | 
						|
    # type: (Dict[str, Any], str) -> Text
 | 
						|
    action += ' on'
 | 
						|
    return get_pull_request_event_message(
 | 
						|
        get_user_username(payload),
 | 
						|
        action,
 | 
						|
        payload['pullrequest']['links']['html']['href'],
 | 
						|
        payload['pullrequest']['id'],
 | 
						|
        message=payload['comment']['content']['raw']
 | 
						|
    )
 | 
						|
 | 
						|
def get_push_tag_body(payload, change):
 | 
						|
    # type: (Dict[str, Any], Dict[str, Any]) -> Text
 | 
						|
    if change.get('created'):
 | 
						|
        tag = change['new']
 | 
						|
        action = 'pushed'  # type: Optional[Text]
 | 
						|
    elif change.get('closed'):
 | 
						|
        tag = change['old']
 | 
						|
        action = 'removed'
 | 
						|
    else:
 | 
						|
        tag = change['new']
 | 
						|
        action = None
 | 
						|
    return get_push_tag_event_message(
 | 
						|
        get_user_username(payload),
 | 
						|
        tag.get('name'),
 | 
						|
        tag_url=tag['links']['html'].get('href'),
 | 
						|
        action=action
 | 
						|
    )
 | 
						|
 | 
						|
def get_pull_request_title(pullrequest_payload):
 | 
						|
    # type: (Dict[str, Any]) -> str
 | 
						|
    return pullrequest_payload['title']
 | 
						|
 | 
						|
def get_pull_request_url(pullrequest_payload):
 | 
						|
    # type: (Dict[str, Any]) -> str
 | 
						|
    return pullrequest_payload['links']['html']['href']
 | 
						|
 | 
						|
def get_repository_url(repository_payload):
 | 
						|
    # type: (Dict[str, Any]) -> str
 | 
						|
    return repository_payload['links']['html']['href']
 | 
						|
 | 
						|
def get_repository_name(repository_payload):
 | 
						|
    # type: (Dict[str, Any]) -> str
 | 
						|
    return repository_payload['name']
 | 
						|
 | 
						|
def get_repository_full_name(repository_payload):
 | 
						|
    # type: (Dict[str, Any]) -> str
 | 
						|
    return repository_payload['full_name']
 | 
						|
 | 
						|
def get_user_display_name(payload):
 | 
						|
    # type: (Dict[str, Any]) -> str
 | 
						|
    return payload['actor']['display_name']
 | 
						|
 | 
						|
def get_user_username(payload):
 | 
						|
    # type: (Dict[str, Any]) -> str
 | 
						|
    return payload['actor']['username']
 | 
						|
 | 
						|
def get_branch_name_for_push_event(payload):
 | 
						|
    # type: (Dict[str, Any]) -> Optional[str]
 | 
						|
    change = payload['push']['changes'][-1]
 | 
						|
    potential_tag = (change['new'] or change['old'] or {}).get('type')
 | 
						|
    if potential_tag == 'tag':
 | 
						|
        return None
 | 
						|
    else:
 | 
						|
        return (change['new'] or change['old']).get('name')
 | 
						|
 | 
						|
GET_SINGLE_MESSAGE_BODY_DEPENDING_ON_TYPE_MAPPER = {
 | 
						|
    'fork': get_fork_body,
 | 
						|
    'commit_comment': get_commit_comment_body,
 | 
						|
    'change_commit_status': get_commit_status_changed_body,
 | 
						|
    'issue_updated': partial(get_issue_action_body, action='updated'),
 | 
						|
    'issue_created': partial(get_issue_action_body, action='created'),
 | 
						|
    'issue_commented': get_issue_commented_body,
 | 
						|
    'pull_request_created': partial(get_pull_request_created_or_updated_body, action='created'),
 | 
						|
    'pull_request_updated': partial(get_pull_request_created_or_updated_body, action='updated'),
 | 
						|
    'pull_request_approved': partial(get_pull_request_action_body, action='approved'),
 | 
						|
    'pull_request_unapproved': partial(get_pull_request_action_body, action='unapproved'),
 | 
						|
    'pull_request_fulfilled': partial(get_pull_request_action_body, action='merged'),
 | 
						|
    'pull_request_rejected': partial(get_pull_request_action_body, action='rejected'),
 | 
						|
    'pull_request_comment_created': get_pull_request_comment_created_action_body,
 | 
						|
    'pull_request_comment_updated': partial(get_pull_request_deleted_or_updated_comment_action_body, action='updated'),
 | 
						|
    'pull_request_comment_deleted': partial(get_pull_request_deleted_or_updated_comment_action_body, action='deleted')
 | 
						|
}
 |