zulip/zerver/webhooks/taiga/view.py

271 lines
11 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
from django.http import HttpRequest, HttpResponse
from zerver.decorator import api_key_only_webhook_view
from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import 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"))