Files
zulip/zerver/webhooks/pivotal/view.py
Anders Kaseorg 365fe0b3d5 python: Sort imports with isort.
Fixes #2665.

Regenerated by tabbott with `lint --fix` after a rebase and change in
parameters.

Note from tabbott: In a few cases, this converts technical debt in the
form of unsorted imports into different technical debt in the form of
our largest files having very long, ugly import sequences at the
start.  I expect this change will increase pressure for us to split
those files, which isn't a bad thing.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
2020-06-11 16:45:32 -07:00

173 lines
7.1 KiB
Python

"""Webhooks for external integrations."""
import re
from typing import Any, Dict, List, Optional, Tuple
import ujson
from defusedxml.ElementTree import fromstring as xml_fromstring
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 has_request_variables
from zerver.lib.response import json_error, json_success
from zerver.lib.webhooks.common import UnexpectedWebhookEventType, check_send_webhook_message
from zerver.models import UserProfile
def api_pivotal_webhook_v3(request: HttpRequest, user_profile: UserProfile) -> Tuple[str, str]:
payload = xml_fromstring(request.body)
def get_text(attrs: List[str]) -> str:
start = payload
try:
for attr in attrs:
start = start.find(attr)
return start.text
except AttributeError:
return ""
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 = f"https://www.pivotaltracker.com/s/projects/{project_id}/stories/{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 = f" [(view)]({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 = f" worth {estimate} story points"
subject = name
content = f"{description} ({issue_status} {issue_type}{estimate}):\n\n~~~ quote\n{issue_desc}\n~~~\n\n{more_info}"
return subject, content
UNSUPPORTED_EVENT_TYPES = [
"task_create_activity",
"comment_delete_activity",
"task_delete_activity",
"task_update_activity",
"story_move_from_project_activity",
"story_delete_activity",
"story_move_into_project_activity",
"epic_update_activity",
]
def api_pivotal_webhook_v5(request: HttpRequest, user_profile: UserProfile) -> Tuple[str, str]:
payload = ujson.loads(request.body)
event_type = payload["kind"]
project_name = payload["project"]["name"]
project_id = payload["project"]["id"]
primary_resources = payload["primary_resources"][0]
story_url = primary_resources["url"]
story_type = primary_resources.get("story_type")
story_id = primary_resources["id"]
story_name = primary_resources["name"]
performed_by = payload.get("performed_by", {}).get("name", "")
story_info = f"[{project_name}](https://www.pivotaltracker.com/s/projects/{project_id}): [{story_name}]({story_url})"
changes = payload.get("changes", [])
content = ""
subject = f"#{story_id}: {story_name}"
def extract_comment(change: Dict[str, Any]) -> Optional[str]:
if change.get("kind") == "comment":
return change.get("new_values", {}).get("text", None)
return None
if event_type == "story_update_activity":
# Find the changed valued and build a message
content += f"{performed_by} updated {story_info}:\n"
for change in changes:
old_values = change.get("original_values", {})
new_values = change["new_values"]
if "current_state" in old_values and "current_state" in new_values:
content += "* state changed from **{}** to **{}**\n".format(
old_values["current_state"], new_values["current_state"])
if "estimate" in old_values and "estimate" in new_values:
old_estimate = old_values.get("estimate", None)
if old_estimate is None:
estimate = "is now"
else:
estimate = f"changed from {old_estimate} to"
new_estimate = new_values["estimate"] if new_values["estimate"] is not None else "0"
content += f"* estimate {estimate} **{new_estimate} points**\n"
if "story_type" in old_values and "story_type" in new_values:
content += "* type changed from **{}** to **{}**\n".format(
old_values["story_type"], new_values["story_type"])
comment = extract_comment(change)
if comment is not None:
content += f"* Comment added:\n~~~quote\n{comment}\n~~~\n"
elif event_type == "comment_create_activity":
for change in changes:
comment = extract_comment(change)
if comment is not None:
content += f"{performed_by} added a comment to {story_info}:\n~~~quote\n{comment}\n~~~"
elif event_type == "story_create_activity":
content += f"{performed_by} created {story_type}: {story_info}\n"
for change in changes:
new_values = change.get("new_values", {})
if "current_state" in new_values:
content += "* State is **{}**\n".format(new_values["current_state"])
if "description" in new_values:
content += "* Description is\n\n> {}".format(new_values["description"])
elif event_type == "story_move_activity":
content = f"{performed_by} moved {story_info}"
for change in changes:
old_values = change.get("original_values", {})
new_values = change["new_values"]
if "current_state" in old_values and "current_state" in new_values:
content += " from **{}** to **{}**.".format(old_values["current_state"],
new_values["current_state"])
elif event_type in UNSUPPORTED_EVENT_TYPES:
# Known but unsupported Pivotal event types
pass
else:
raise UnexpectedWebhookEventType('Pivotal Tracker', event_type)
return subject, content
@api_key_only_webhook_view("Pivotal")
@has_request_variables
def api_pivotal_webhook(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
subject = content = None
try:
subject, content = api_pivotal_webhook_v3(request, user_profile)
except Exception:
# Attempt to parse v5 JSON payload
subject, content = api_pivotal_webhook_v5(request, user_profile)
if not content:
return json_error(_("Unable to handle Pivotal payload"))
check_send_webhook_message(request, user_profile, subject, content)
return json_success()