mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	This fixes the fact that these emoji were sometimes not displaying properly (because of changes in the emoji names used in the codebase), while also making this integration more standard (since it was the only one with such an aggressive use of emoji).
		
			
				
	
	
		
			273 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			273 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""Taiga integration for Zulip.
 | 
						|
 | 
						|
Tips for notification output:
 | 
						|
 | 
						|
*Text formatting*: if there has been a change of a property, the new
 | 
						|
value should always be in bold; otherwise the subject of US/task
 | 
						|
should be in bold.
 | 
						|
"""
 | 
						|
 | 
						|
from typing import Any, Dict, List, Mapping, Optional, Tuple
 | 
						|
 | 
						|
import ujson
 | 
						|
from django.http import HttpRequest, HttpResponse
 | 
						|
from django.utils.translation import ugettext as _
 | 
						|
 | 
						|
from zerver.decorator import api_key_only_webhook_view
 | 
						|
from zerver.lib.request import REQ, has_request_variables
 | 
						|
from zerver.lib.response import json_error, json_success
 | 
						|
from zerver.lib.webhooks.common import check_send_webhook_message
 | 
						|
from zerver.models import UserProfile
 | 
						|
 | 
						|
@api_key_only_webhook_view('Taiga')
 | 
						|
@has_request_variables
 | 
						|
def api_taiga_webhook(request: HttpRequest, user_profile: UserProfile,
 | 
						|
                      message: Dict[str, Any]=REQ(argument_type='body')) -> HttpResponse:
 | 
						|
    parsed_events = parse_message(message)
 | 
						|
 | 
						|
    content_lines = []
 | 
						|
    for event in parsed_events:
 | 
						|
        content_lines.append(generate_content(event) + '\n')
 | 
						|
    content = "".join(sorted(content_lines))
 | 
						|
    topic = 'General'
 | 
						|
 | 
						|
    check_send_webhook_message(request, user_profile, topic, content)
 | 
						|
 | 
						|
    return json_success()
 | 
						|
 | 
						|
templates = {
 | 
						|
    'epic': {
 | 
						|
        'create': u'%(user)s created epic **%(subject)s**',
 | 
						|
        'set_assigned_to': u'%(user)s assigned epic **%(subject)s** to %(new)s.',
 | 
						|
        'unset_assigned_to': u'%(user)s unassigned epic **%(subject)s**',
 | 
						|
        'changed_assigned_to': u'%(user)s reassigned epic **%(subject)s**'
 | 
						|
        ' from %(old)s to %(new)s.',
 | 
						|
        'blocked': u'%(user)s blocked epic **%(subject)s**',
 | 
						|
        'unblocked': u'%(user)s unblocked epic **%(subject)s**',
 | 
						|
        'changed_status': u'%(user)s changed status of epic **%(subject)s**'
 | 
						|
        ' from %(old)s to %(new)s.',
 | 
						|
        'renamed': u'%(user)s renamed epic from **%(old)s** to **%(new)s**',
 | 
						|
        'description_diff': u'%(user)s updated description of epic **%(subject)s**',
 | 
						|
        'commented': u'%(user)s commented on epic **%(subject)s**',
 | 
						|
        'delete': u'%(user)s deleted epic **%(subject)s**',
 | 
						|
    },
 | 
						|
    'relateduserstory': {
 | 
						|
        'create': (u'%(user)s added a related user story '
 | 
						|
                   u'**%(userstory_subject)s** to the epic **%(epic_subject)s**'),
 | 
						|
        'delete': (u'%(user)s removed a related user story ' +
 | 
						|
                   u'**%(userstory_subject)s** from the epic **%(epic_subject)s**'),
 | 
						|
    },
 | 
						|
    'userstory': {
 | 
						|
        'create': u'%(user)s created user story **%(subject)s**.',
 | 
						|
        'set_assigned_to': u'%(user)s assigned user story **%(subject)s** to %(new)s.',
 | 
						|
        'unset_assigned_to': u'%(user)s unassigned user story **%(subject)s**.',
 | 
						|
        'changed_assigned_to': u'%(user)s reassigned user story **%(subject)s**'
 | 
						|
        ' from %(old)s to %(new)s.',
 | 
						|
        'points': u'%(user)s changed estimation of user story **%(subject)s**.',
 | 
						|
        'blocked': u'%(user)s blocked user story **%(subject)s**.',
 | 
						|
        'unblocked': u'%(user)s unblocked user story **%(subject)s**.',
 | 
						|
        'set_milestone': u'%(user)s added user story **%(subject)s** to sprint %(new)s.',
 | 
						|
        'unset_milestone': u'%(user)s removed user story **%(subject)s** from sprint %(old)s.',
 | 
						|
        'changed_milestone': u'%(user)s changed sprint of user story **%(subject)s** from %(old)s'
 | 
						|
        ' to %(new)s.',
 | 
						|
        'changed_status': u'%(user)s changed status of user story **%(subject)s**'
 | 
						|
        ' from %(old)s to %(new)s.',
 | 
						|
        'closed': u'%(user)s closed user story **%(subject)s**.',
 | 
						|
        'reopened': u'%(user)s reopened user story **%(subject)s**.',
 | 
						|
        'renamed': u'%(user)s renamed user story from %(old)s to **%(new)s**.',
 | 
						|
        'description_diff': u'%(user)s updated description of user story **%(subject)s**.',
 | 
						|
        'commented': u'%(user)s commented on user story **%(subject)s**.',
 | 
						|
        'delete': u'%(user)s deleted user story **%(subject)s**.'
 | 
						|
    },
 | 
						|
    'milestone': {
 | 
						|
        'create': u'%(user)s created sprint **%(subject)s**.',
 | 
						|
        'renamed': u'%(user)s renamed sprint from %(old)s to **%(new)s**.',
 | 
						|
        'estimated_start': u'%(user)s changed estimated start of sprint **%(subject)s**'
 | 
						|
        ' from %(old)s to %(new)s.',
 | 
						|
        'estimated_finish': u'%(user)s changed estimated finish of sprint **%(subject)s**'
 | 
						|
        ' from %(old)s to %(new)s.',
 | 
						|
        'delete': u'%(user)s deleted sprint **%(subject)s**.'
 | 
						|
    },
 | 
						|
    'task': {
 | 
						|
        'create': u'%(user)s created task **%(subject)s**.',
 | 
						|
        'set_assigned_to': u'%(user)s assigned task **%(subject)s** to %(new)s.',
 | 
						|
        'unset_assigned_to': u'%(user)s unassigned task **%(subject)s**.',
 | 
						|
        'changed_assigned_to': u'%(user)s reassigned task **%(subject)s**'
 | 
						|
        ' from %(old)s to %(new)s.',
 | 
						|
        'blocked': u'%(user)s blocked task **%(subject)s**.',
 | 
						|
        'unblocked': u'%(user)s unblocked task **%(subject)s**.',
 | 
						|
        'set_milestone': u'%(user)s added task **%(subject)s** to sprint %(new)s.',
 | 
						|
        'changed_milestone': u'%(user)s changed sprint of task '
 | 
						|
                             '**%(subject)s** from %(old)s to %(new)s.',
 | 
						|
        'changed_status': u'%(user)s changed status of task **%(subject)s**'
 | 
						|
        ' from %(old)s to %(new)s.',
 | 
						|
        'renamed': u'%(user)s renamed task %(old)s to **%(new)s**.',
 | 
						|
        'description_diff': u'%(user)s updated description of task **%(subject)s**.',
 | 
						|
        'commented': u'%(user)s commented on task **%(subject)s**.',
 | 
						|
        'delete': u'%(user)s deleted task **%(subject)s**.',
 | 
						|
        'changed_us': u'%(user)s moved task **%(subject)s** from user story %(old)s to %(new)s.'
 | 
						|
    },
 | 
						|
    'issue': {
 | 
						|
        'create': u'%(user)s created issue **%(subject)s**.',
 | 
						|
        'set_assigned_to': u'%(user)s assigned issue **%(subject)s** to %(new)s.',
 | 
						|
        'unset_assigned_to': u'%(user)s unassigned issue **%(subject)s**.',
 | 
						|
        'changed_assigned_to': u'%(user)s reassigned issue **%(subject)s**'
 | 
						|
        ' from %(old)s to %(new)s.',
 | 
						|
        'changed_priority': u'%(user)s changed priority of issue '
 | 
						|
                            '**%(subject)s** from %(old)s to %(new)s.',
 | 
						|
        'changed_severity': u'%(user)s changed severity of issue '
 | 
						|
                            '**%(subject)s** from %(old)s to %(new)s.',
 | 
						|
        'changed_status': u'%(user)s changed status of issue **%(subject)s**'
 | 
						|
                           ' from %(old)s to %(new)s.',
 | 
						|
        'changed_type': u'%(user)s changed type of issue **%(subject)s** from %(old)s to %(new)s.',
 | 
						|
        'renamed': u'%(user)s renamed issue %(old)s to **%(new)s**.',
 | 
						|
        'description_diff': u'%(user)s updated description of issue **%(subject)s**.',
 | 
						|
        'commented': u'%(user)s commented on issue **%(subject)s**.',
 | 
						|
        'delete': u'%(user)s deleted issue **%(subject)s**.'
 | 
						|
    },
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
return_type = Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]
 | 
						|
def get_old_and_new_values(change_type: str,
 | 
						|
                           message: Mapping[str, Any]) -> return_type:
 | 
						|
    """ Parses the payload and finds previous and current value of change_type."""
 | 
						|
    if change_type in ['subject', 'name', 'estimated_finish', 'estimated_start']:
 | 
						|
        old = message["change"]["diff"][change_type]["from"]
 | 
						|
        new = message["change"]["diff"][change_type]["to"]
 | 
						|
        return old, new
 | 
						|
 | 
						|
    old = message["change"]["diff"][change_type].get("from")
 | 
						|
    new = message["change"]["diff"][change_type].get("to")
 | 
						|
 | 
						|
    return old, new
 | 
						|
 | 
						|
 | 
						|
def parse_comment(message: Mapping[str, Any]) -> Dict[str, Any]:
 | 
						|
    """ Parses the comment to issue, task or US. """
 | 
						|
    return {
 | 
						|
        'event': 'commented',
 | 
						|
        'type': message["type"],
 | 
						|
        'values': {
 | 
						|
            'user': get_owner_name(message),
 | 
						|
            'subject': get_subject(message)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
def parse_create_or_delete(message: Mapping[str, Any]) -> Dict[str, Any]:
 | 
						|
    """ Parses create or delete event. """
 | 
						|
    if message["type"] == 'relateduserstory':
 | 
						|
        return {
 | 
						|
            'type': message["type"],
 | 
						|
            'event': message["action"],
 | 
						|
            'values': {
 | 
						|
                'user': get_owner_name(message),
 | 
						|
                'epic_subject': message['data']['epic']['subject'],
 | 
						|
                'userstory_subject': message['data']['user_story']['subject'],
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
    return {
 | 
						|
        'type': message["type"],
 | 
						|
        'event': message["action"],
 | 
						|
        'values': {
 | 
						|
            'user': get_owner_name(message),
 | 
						|
            'subject': get_subject(message)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
def parse_change_event(change_type: str, message: Mapping[str, Any]) -> Optional[Dict[str, Any]]:
 | 
						|
    """ Parses change event. """
 | 
						|
    evt = {}  # type: Dict[str, Any]
 | 
						|
    values = {
 | 
						|
        'user': get_owner_name(message),
 | 
						|
        'subject': get_subject(message)
 | 
						|
    }  # type: Dict[str, Any]
 | 
						|
 | 
						|
    if change_type in ["description_diff", "points"]:
 | 
						|
        event_type = change_type
 | 
						|
 | 
						|
    elif change_type in ["milestone", "assigned_to"]:
 | 
						|
        old, new = get_old_and_new_values(change_type, message)
 | 
						|
        if not old:
 | 
						|
            event_type = "set_" + change_type
 | 
						|
            values["new"] = new
 | 
						|
        elif not new:
 | 
						|
            event_type = "unset_" + change_type
 | 
						|
            values["old"] = old
 | 
						|
        else:
 | 
						|
            event_type = "changed_" + change_type
 | 
						|
            values.update({'old': old, 'new': new})
 | 
						|
 | 
						|
    elif change_type == "is_blocked":
 | 
						|
        if message["change"]["diff"]["is_blocked"]["to"]:
 | 
						|
            event_type = "blocked"
 | 
						|
        else:
 | 
						|
            event_type = "unblocked"
 | 
						|
 | 
						|
    elif change_type == "is_closed":
 | 
						|
        if message["change"]["diff"]["is_closed"]["to"]:
 | 
						|
            event_type = "closed"
 | 
						|
        else:
 | 
						|
            event_type = "reopened"
 | 
						|
 | 
						|
    elif change_type == "user_story":
 | 
						|
        old, new = get_old_and_new_values(change_type, message)
 | 
						|
        event_type = "changed_us"
 | 
						|
        values.update({'old': old, 'new': new})
 | 
						|
 | 
						|
    elif change_type in ["subject", 'name']:
 | 
						|
        event_type = 'renamed'
 | 
						|
        old, new = get_old_and_new_values(change_type, message)
 | 
						|
        values.update({'old': old, 'new': new})
 | 
						|
 | 
						|
    elif change_type in ["estimated_finish", "estimated_start"]:
 | 
						|
        old, new = get_old_and_new_values(change_type, message)
 | 
						|
        if not old == new:
 | 
						|
            event_type = change_type
 | 
						|
            values.update({'old': old, 'new': new})
 | 
						|
        else:
 | 
						|
            # date hasn't changed
 | 
						|
            return None
 | 
						|
 | 
						|
    elif change_type in ["priority", "severity", "type", "status"]:
 | 
						|
        event_type = 'changed_' + change_type
 | 
						|
        old, new = get_old_and_new_values(change_type, message)
 | 
						|
        values.update({'old': old, 'new': new})
 | 
						|
 | 
						|
    else:
 | 
						|
        # we are not supporting this type of event
 | 
						|
        return None
 | 
						|
 | 
						|
    evt.update({"type": message["type"], "event": event_type, "values": values})
 | 
						|
    return evt
 | 
						|
 | 
						|
 | 
						|
def parse_message(message: Mapping[str, Any]) -> List[Dict[str, Any]]:
 | 
						|
    """ Parses the payload by delegating to specialized functions. """
 | 
						|
    events = []
 | 
						|
    if message["action"] in ['create', 'delete']:
 | 
						|
        events.append(parse_create_or_delete(message))
 | 
						|
    elif message["action"] == 'change':
 | 
						|
        if message["change"]["diff"]:
 | 
						|
            for value in message["change"]["diff"]:
 | 
						|
                parsed_event = parse_change_event(value, message)
 | 
						|
                if parsed_event:
 | 
						|
                    events.append(parsed_event)
 | 
						|
        if message["change"]["comment"]:
 | 
						|
            events.append(parse_comment(message))
 | 
						|
 | 
						|
    return events
 | 
						|
 | 
						|
def generate_content(data: Mapping[str, Any]) -> str:
 | 
						|
    """ Gets the template string and formats it with parsed data. """
 | 
						|
    return templates[data['type']][data['event']] % data['values']
 | 
						|
 | 
						|
def get_owner_name(message: Mapping[str, Any]) -> str:
 | 
						|
    return message["by"]["full_name"]
 | 
						|
 | 
						|
def get_subject(message: Mapping[str, Any]) -> str:
 | 
						|
    data = message["data"]
 | 
						|
    return data.get("subject", data.get("name"))
 |