We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Pivotal integration exception
Hi, 
I've added Pivotal integration and from time to time I receive those two e-mails when working in Pivotal:
I'm running ubuntu 20.04

If you need more information, I'd be happy to help.

```
Logger django.request, from module django.utils.log line 224:
Error generated by PivotalMessenger <pivotal-bot@***> (Member) on *** deployment

No stack trace available

Deployed code:
- git: None
- ZULIP_VERSION: 4.2


Request info:
- path: /api/v1/external/pivotal
- POST: {}
- REMOTE_ADDR: "35.184.18.147"
- QUERY_STRING: "api_key=******&stream=******&topic=******"
- SERVER_NAME: ""
```

```
Logger zerver.middleware.json_error_handler, from module zerver.middleware line 450:
Error generated by PivotalMessenger <pivotal-bot@***> (Member) on *** deployment

Traceback (most recent call last):
  File "/usr/lib/python3.8/xml/etree/ElementTree.py", line 1693, in feed
    self.parser.Parse(data, 0)
xml.parsers.expat.ExpatError: not well-formed (invalid token): line 1, column 0

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "./zerver/webhooks/pivotal/view.py", line 172, in api_pivotal_webhook
    subject, content = api_pivotal_webhook_v3(request, user_profile)
  File "./zerver/webhooks/pivotal/view.py", line 19, in api_pivotal_webhook_v3
    payload = xml_fromstring(request.body)
  File "/srv/zulip-venv-cache/9d0f5ac272f4e644b222ed65b0b5a996616a215f/zulip-py3-venv/lib/python3.8/site-packages/defusedxml/common.py", line 131, in fromstring
    parser.feed(text)
  File "/usr/lib/python3.8/xml/etree/ElementTree.py", line 1695, in feed
    self._raiseerror(v)
  File "/usr/lib/python3.8/xml/etree/ElementTree.py", line 1602, in _raiseerror
    raise err
  File "<string>", line None
xml.etree.ElementTree.ParseError: not well-formed (invalid token): line 1, column 0

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/srv/zulip-venv-cache/9d0f5ac272f4e644b222ed65b0b5a996616a215f/zulip-py3-venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/srv/zulip-venv-cache/9d0f5ac272f4e644b222ed65b0b5a996616a215f/zulip-py3-venv/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "./zerver/lib/request.py", line 390, in _wrapped_view_func
    return view_func(request, *args, **kwargs)
  File "./zerver/decorator.py", line 354, in _wrapped_func_arguments
    raise err
  File "./zerver/decorator.py", line 334, in _wrapped_func_arguments
    return view_func(request, user_profile, *args, **kwargs)
  File "./zerver/lib/request.py", line 390, in _wrapped_view_func
    return view_func(request, *args, **kwargs)
  File "./zerver/webhooks/pivotal/view.py", line 175, in api_pivotal_webhook
    subject, content = api_pivotal_webhook_v5(request, user_profile)
  File "./zerver/webhooks/pivotal/view.py", line 87, in api_pivotal_webhook_v5
    story_url = primary_resources["url"]
KeyError: 'url'


Deployed code:
- git: None
- ZULIP_VERSION: 4.2


Request info:
- path: /api/v1/external/pivotal
- POST: {}
- REMOTE_ADDR: "35.184.18.147"
- QUERY_STRING: "api_key=******&stream=******&topic=******"
- SERVER_NAME: ""


```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `zerver/webhooks/pivotal/view.py`
Content:
```
1 """Webhooks for external integrations."""
2 import re
3 from typing import Any, Dict, List, Optional, Tuple
4 
5 import orjson
6 from defusedxml.ElementTree import fromstring as xml_fromstring
7 from django.http import HttpRequest, HttpResponse
8 from django.utils.translation import gettext as _
9 
10 from zerver.decorator import webhook_view
11 from zerver.lib.exceptions import UnsupportedWebhookEventType
12 from zerver.lib.request import has_request_variables
13 from zerver.lib.response import json_error, json_success
14 from zerver.lib.webhooks.common import check_send_webhook_message
15 from zerver.models import UserProfile
16 
17 
18 def api_pivotal_webhook_v3(request: HttpRequest, user_profile: UserProfile) -> Tuple[str, str]:
19     payload = xml_fromstring(request.body)
20 
21     def get_text(attrs: List[str]) -> str:
22         start = payload
23         try:
24             for attr in attrs:
25                 start = start.find(attr)
26             return start.text
27         except AttributeError:
28             return ""
29 
30     event_type = payload.find("event_type").text
31     description = payload.find("description").text
32     project_id = payload.find("project_id").text
33     story_id = get_text(["stories", "story", "id"])
34     # Ugh, the URL in the XML data is not a clickable URL that works for the user
35     # so we try to build one that the user can actually click on
36     url = f"https://www.pivotaltracker.com/s/projects/{project_id}/stories/{story_id}"
37 
38     # Pivotal doesn't tell us the name of the story, but it's usually in the
39     # description in quotes as the first quoted string
40     name_re = re.compile(r'[^"]+"([^"]+)".*')
41     match = name_re.match(description)
42     if match and len(match.groups()):
43         name = match.group(1)
44     else:
45         name = "Story changed"  # Failed for an unknown reason, show something
46     more_info = f" [(view)]({url})."
47 
48     if event_type == "story_update":
49         subject = name
50         content = description + more_info
51     elif event_type == "note_create":
52         subject = "Comment added"
53         content = description + more_info
54     elif event_type == "story_create":
55         issue_desc = get_text(["stories", "story", "description"])
56         issue_type = get_text(["stories", "story", "story_type"])
57         issue_status = get_text(["stories", "story", "current_state"])
58         estimate = get_text(["stories", "story", "estimate"])
59         if estimate != "":
60             estimate = f" worth {estimate} story points"
61         subject = name
62         content = f"{description} ({issue_status} {issue_type}{estimate}):\n\n~~~ quote\n{issue_desc}\n~~~\n\n{more_info}"
63     return subject, content
64 
65 
66 UNSUPPORTED_EVENT_TYPES = [
67     "task_create_activity",
68     "comment_delete_activity",
69     "task_delete_activity",
70     "task_update_activity",
71     "story_move_from_project_activity",
72     "story_delete_activity",
73     "story_move_into_project_activity",
74     "epic_update_activity",
75 ]
76 
77 
78 def api_pivotal_webhook_v5(request: HttpRequest, user_profile: UserProfile) -> Tuple[str, str]:
79     payload = orjson.loads(request.body)
80 
81     event_type = payload["kind"]
82 
83     project_name = payload["project"]["name"]
84     project_id = payload["project"]["id"]
85 
86     primary_resources = payload["primary_resources"][0]
87     story_url = primary_resources["url"]
88     story_type = primary_resources.get("story_type")
89     story_id = primary_resources["id"]
90     story_name = primary_resources["name"]
91 
92     performed_by = payload.get("performed_by", {}).get("name", "")
93 
94     story_info = f"[{project_name}](https://www.pivotaltracker.com/s/projects/{project_id}): [{story_name}]({story_url})"
95 
96     changes = payload.get("changes", [])
97 
98     content = ""
99     subject = f"#{story_id}: {story_name}"
100 
101     def extract_comment(change: Dict[str, Any]) -> Optional[str]:
102         if change.get("kind") == "comment":
103             return change.get("new_values", {}).get("text", None)
104         return None
105 
106     if event_type == "story_update_activity":
107         # Find the changed valued and build a message
108         content += f"{performed_by} updated {story_info}:\n"
109         for change in changes:
110             old_values = change.get("original_values", {})
111             new_values = change["new_values"]
112 
113             if "current_state" in old_values and "current_state" in new_values:
114                 content += "* state changed from **{}** to **{}**\n".format(
115                     old_values["current_state"], new_values["current_state"]
116                 )
117             if "estimate" in old_values and "estimate" in new_values:
118                 old_estimate = old_values.get("estimate", None)
119                 if old_estimate is None:
120                     estimate = "is now"
121                 else:
122                     estimate = f"changed from {old_estimate} to"
123                 new_estimate = new_values["estimate"] if new_values["estimate"] is not None else "0"
124                 content += f"* estimate {estimate} **{new_estimate} points**\n"
125             if "story_type" in old_values and "story_type" in new_values:
126                 content += "* type changed from **{}** to **{}**\n".format(
127                     old_values["story_type"], new_values["story_type"]
128                 )
129 
130             comment = extract_comment(change)
131             if comment is not None:
132                 content += f"* Comment added:\n~~~quote\n{comment}\n~~~\n"
133 
134     elif event_type == "comment_create_activity":
135         for change in changes:
136             comment = extract_comment(change)
137             if comment is not None:
138                 content += (
139                     f"{performed_by} added a comment to {story_info}:\n~~~quote\n{comment}\n~~~"
140                 )
141     elif event_type == "story_create_activity":
142         content += f"{performed_by} created {story_type}: {story_info}\n"
143         for change in changes:
144             new_values = change.get("new_values", {})
145             if "current_state" in new_values:
146                 content += "* State is **{}**\n".format(new_values["current_state"])
147             if "description" in new_values:
148                 content += "* Description is\n\n> {}".format(new_values["description"])
149     elif event_type == "story_move_activity":
150         content = f"{performed_by} moved {story_info}"
151         for change in changes:
152             old_values = change.get("original_values", {})
153             new_values = change["new_values"]
154             if "current_state" in old_values and "current_state" in new_values:
155                 content += " from **{}** to **{}**.".format(
156                     old_values["current_state"], new_values["current_state"]
157                 )
158     elif event_type in UNSUPPORTED_EVENT_TYPES:
159         # Known but unsupported Pivotal event types
160         pass
161     else:
162         raise UnsupportedWebhookEventType(event_type)
163 
164     return subject, content
165 
166 
167 @webhook_view("Pivotal")
168 @has_request_variables
169 def api_pivotal_webhook(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
170     subject = content = None
171     try:
172         subject, content = api_pivotal_webhook_v3(request, user_profile)
173     except Exception:
174         # Attempt to parse v5 JSON payload
175         subject, content = api_pivotal_webhook_v5(request, user_profile)
176 
177     if not content:
178         return json_error(_("Unable to handle Pivotal payload"))
179 
180     check_send_webhook_message(request, user_profile, subject, content)
181     return json_success()
182
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/zerver/webhooks/pivotal/view.py b/zerver/webhooks/pivotal/view.py
--- a/zerver/webhooks/pivotal/view.py
+++ b/zerver/webhooks/pivotal/view.py
@@ -72,6 +72,7 @@
     "story_delete_activity",
     "story_move_into_project_activity",
     "epic_update_activity",
+    "label_create_activity",
 ]

{"golden_diff": "diff --git a/zerver/webhooks/pivotal/view.py b/zerver/webhooks/pivotal/view.py\n--- a/zerver/webhooks/pivotal/view.py\n+++ b/zerver/webhooks/pivotal/view.py\n@@ -72,6 +72,7 @@\n     \"story_delete_activity\",\n     \"story_move_into_project_activity\",\n     \"epic_update_activity\",\n+    \"label_create_activity\",\n ]\n", "issue": "Pivotal integration exception\nHi, \r\nI've added Pivotal integration and from time to time I receive those two e-mails when working in Pivotal:\r\nI'm running ubuntu 20.04\r\n\r\nIf you need more information, I'd be happy to help.\r\n\r\n```\r\nLogger django.request, from module django.utils.log line 224:\r\nError generated by PivotalMessenger <pivotal-bot@***> (Member) on *** deployment\r\n\r\nNo stack trace available\r\n\r\nDeployed code:\r\n- git: None\r\n- ZULIP_VERSION: 4.2\r\n\r\n\r\nRequest info:\r\n- path: /api/v1/external/pivotal\r\n- POST: {}\r\n- REMOTE_ADDR: \"35.184.18.147\"\r\n- QUERY_STRING: \"api_key=******&stream=******&topic=******\"\r\n- SERVER_NAME: \"\"\r\n```\r\n\r\n```\r\nLogger zerver.middleware.json_error_handler, from module zerver.middleware line 450:\r\nError generated by PivotalMessenger <pivotal-bot@***> (Member) on *** deployment\r\n\r\nTraceback (most recent call last):\r\n  File \"/usr/lib/python3.8/xml/etree/ElementTree.py\", line 1693, in feed\r\n    self.parser.Parse(data, 0)\r\nxml.parsers.expat.ExpatError: not well-formed (invalid token): line 1, column 0\r\n\r\nDuring handling of the above exception, another exception occurred:\r\n\r\nTraceback (most recent call last):\r\n  File \"./zerver/webhooks/pivotal/view.py\", line 172, in api_pivotal_webhook\r\n    subject, content = api_pivotal_webhook_v3(request, user_profile)\r\n  File \"./zerver/webhooks/pivotal/view.py\", line 19, in api_pivotal_webhook_v3\r\n    payload = xml_fromstring(request.body)\r\n  File \"/srv/zulip-venv-cache/9d0f5ac272f4e644b222ed65b0b5a996616a215f/zulip-py3-venv/lib/python3.8/site-packages/defusedxml/common.py\", line 131, in fromstring\r\n    parser.feed(text)\r\n  File \"/usr/lib/python3.8/xml/etree/ElementTree.py\", line 1695, in feed\r\n    self._raiseerror(v)\r\n  File \"/usr/lib/python3.8/xml/etree/ElementTree.py\", line 1602, in _raiseerror\r\n    raise err\r\n  File \"<string>\", line None\r\nxml.etree.ElementTree.ParseError: not well-formed (invalid token): line 1, column 0\r\n\r\nDuring handling of the above exception, another exception occurred:\r\n\r\nTraceback (most recent call last):\r\n  File \"/srv/zulip-venv-cache/9d0f5ac272f4e644b222ed65b0b5a996616a215f/zulip-py3-venv/lib/python3.8/site-packages/django/core/handlers/base.py\", line 181, in _get_response\r\n    response = wrapped_callback(request, *callback_args, **callback_kwargs)\r\n  File \"/srv/zulip-venv-cache/9d0f5ac272f4e644b222ed65b0b5a996616a215f/zulip-py3-venv/lib/python3.8/site-packages/django/views/decorators/csrf.py\", line 54, in wrapped_view\r\n    return view_func(*args, **kwargs)\r\n  File \"./zerver/lib/request.py\", line 390, in _wrapped_view_func\r\n    return view_func(request, *args, **kwargs)\r\n  File \"./zerver/decorator.py\", line 354, in _wrapped_func_arguments\r\n    raise err\r\n  File \"./zerver/decorator.py\", line 334, in _wrapped_func_arguments\r\n    return view_func(request, user_profile, *args, **kwargs)\r\n  File \"./zerver/lib/request.py\", line 390, in _wrapped_view_func\r\n    return view_func(request, *args, **kwargs)\r\n  File \"./zerver/webhooks/pivotal/view.py\", line 175, in api_pivotal_webhook\r\n    subject, content = api_pivotal_webhook_v5(request, user_profile)\r\n  File \"./zerver/webhooks/pivotal/view.py\", line 87, in api_pivotal_webhook_v5\r\n    story_url = primary_resources[\"url\"]\r\nKeyError: 'url'\r\n\r\n\r\nDeployed code:\r\n- git: None\r\n- ZULIP_VERSION: 4.2\r\n\r\n\r\nRequest info:\r\n- path: /api/v1/external/pivotal\r\n- POST: {}\r\n- REMOTE_ADDR: \"35.184.18.147\"\r\n- QUERY_STRING: \"api_key=******&stream=******&topic=******\"\r\n- SERVER_NAME: \"\"\r\n\r\n\r\n```\n", "before_files": [{"content": "\"\"\"Webhooks for external integrations.\"\"\"\nimport re\nfrom typing import Any, Dict, List, Optional, Tuple\n\nimport orjson\nfrom defusedxml.ElementTree import fromstring as xml_fromstring\nfrom django.http import HttpRequest, HttpResponse\nfrom django.utils.translation import gettext as _\n\nfrom zerver.decorator import webhook_view\nfrom zerver.lib.exceptions import UnsupportedWebhookEventType\nfrom zerver.lib.request import has_request_variables\nfrom zerver.lib.response import json_error, json_success\nfrom zerver.lib.webhooks.common import check_send_webhook_message\nfrom zerver.models import UserProfile\n\n\ndef api_pivotal_webhook_v3(request: HttpRequest, user_profile: UserProfile) -> Tuple[str, str]:\n    payload = xml_fromstring(request.body)\n\n    def get_text(attrs: List[str]) -> str:\n        start = payload\n        try:\n            for attr in attrs:\n                start = start.find(attr)\n            return start.text\n        except AttributeError:\n            return \"\"\n\n    event_type = payload.find(\"event_type\").text\n    description = payload.find(\"description\").text\n    project_id = payload.find(\"project_id\").text\n    story_id = get_text([\"stories\", \"story\", \"id\"])\n    # Ugh, the URL in the XML data is not a clickable URL that works for the user\n    # so we try to build one that the user can actually click on\n    url = f\"https://www.pivotaltracker.com/s/projects/{project_id}/stories/{story_id}\"\n\n    # Pivotal doesn't tell us the name of the story, but it's usually in the\n    # description in quotes as the first quoted string\n    name_re = re.compile(r'[^\"]+\"([^\"]+)\".*')\n    match = name_re.match(description)\n    if match and len(match.groups()):\n        name = match.group(1)\n    else:\n        name = \"Story changed\"  # Failed for an unknown reason, show something\n    more_info = f\" [(view)]({url}).\"\n\n    if event_type == \"story_update\":\n        subject = name\n        content = description + more_info\n    elif event_type == \"note_create\":\n        subject = \"Comment added\"\n        content = description + more_info\n    elif event_type == \"story_create\":\n        issue_desc = get_text([\"stories\", \"story\", \"description\"])\n        issue_type = get_text([\"stories\", \"story\", \"story_type\"])\n        issue_status = get_text([\"stories\", \"story\", \"current_state\"])\n        estimate = get_text([\"stories\", \"story\", \"estimate\"])\n        if estimate != \"\":\n            estimate = f\" worth {estimate} story points\"\n        subject = name\n        content = f\"{description} ({issue_status} {issue_type}{estimate}):\\n\\n~~~ quote\\n{issue_desc}\\n~~~\\n\\n{more_info}\"\n    return subject, content\n\n\nUNSUPPORTED_EVENT_TYPES = [\n    \"task_create_activity\",\n    \"comment_delete_activity\",\n    \"task_delete_activity\",\n    \"task_update_activity\",\n    \"story_move_from_project_activity\",\n    \"story_delete_activity\",\n    \"story_move_into_project_activity\",\n    \"epic_update_activity\",\n]\n\n\ndef api_pivotal_webhook_v5(request: HttpRequest, user_profile: UserProfile) -> Tuple[str, str]:\n    payload = orjson.loads(request.body)\n\n    event_type = payload[\"kind\"]\n\n    project_name = payload[\"project\"][\"name\"]\n    project_id = payload[\"project\"][\"id\"]\n\n    primary_resources = payload[\"primary_resources\"][0]\n    story_url = primary_resources[\"url\"]\n    story_type = primary_resources.get(\"story_type\")\n    story_id = primary_resources[\"id\"]\n    story_name = primary_resources[\"name\"]\n\n    performed_by = payload.get(\"performed_by\", {}).get(\"name\", \"\")\n\n    story_info = f\"[{project_name}](https://www.pivotaltracker.com/s/projects/{project_id}): [{story_name}]({story_url})\"\n\n    changes = payload.get(\"changes\", [])\n\n    content = \"\"\n    subject = f\"#{story_id}: {story_name}\"\n\n    def extract_comment(change: Dict[str, Any]) -> Optional[str]:\n        if change.get(\"kind\") == \"comment\":\n            return change.get(\"new_values\", {}).get(\"text\", None)\n        return None\n\n    if event_type == \"story_update_activity\":\n        # Find the changed valued and build a message\n        content += f\"{performed_by} updated {story_info}:\\n\"\n        for change in changes:\n            old_values = change.get(\"original_values\", {})\n            new_values = change[\"new_values\"]\n\n            if \"current_state\" in old_values and \"current_state\" in new_values:\n                content += \"* state changed from **{}** to **{}**\\n\".format(\n                    old_values[\"current_state\"], new_values[\"current_state\"]\n                )\n            if \"estimate\" in old_values and \"estimate\" in new_values:\n                old_estimate = old_values.get(\"estimate\", None)\n                if old_estimate is None:\n                    estimate = \"is now\"\n                else:\n                    estimate = f\"changed from {old_estimate} to\"\n                new_estimate = new_values[\"estimate\"] if new_values[\"estimate\"] is not None else \"0\"\n                content += f\"* estimate {estimate} **{new_estimate} points**\\n\"\n            if \"story_type\" in old_values and \"story_type\" in new_values:\n                content += \"* type changed from **{}** to **{}**\\n\".format(\n                    old_values[\"story_type\"], new_values[\"story_type\"]\n                )\n\n            comment = extract_comment(change)\n            if comment is not None:\n                content += f\"* Comment added:\\n~~~quote\\n{comment}\\n~~~\\n\"\n\n    elif event_type == \"comment_create_activity\":\n        for change in changes:\n            comment = extract_comment(change)\n            if comment is not None:\n                content += (\n                    f\"{performed_by} added a comment to {story_info}:\\n~~~quote\\n{comment}\\n~~~\"\n                )\n    elif event_type == \"story_create_activity\":\n        content += f\"{performed_by} created {story_type}: {story_info}\\n\"\n        for change in changes:\n            new_values = change.get(\"new_values\", {})\n            if \"current_state\" in new_values:\n                content += \"* State is **{}**\\n\".format(new_values[\"current_state\"])\n            if \"description\" in new_values:\n                content += \"* Description is\\n\\n> {}\".format(new_values[\"description\"])\n    elif event_type == \"story_move_activity\":\n        content = f\"{performed_by} moved {story_info}\"\n        for change in changes:\n            old_values = change.get(\"original_values\", {})\n            new_values = change[\"new_values\"]\n            if \"current_state\" in old_values and \"current_state\" in new_values:\n                content += \" from **{}** to **{}**.\".format(\n                    old_values[\"current_state\"], new_values[\"current_state\"]\n                )\n    elif event_type in UNSUPPORTED_EVENT_TYPES:\n        # Known but unsupported Pivotal event types\n        pass\n    else:\n        raise UnsupportedWebhookEventType(event_type)\n\n    return subject, content\n\n\n@webhook_view(\"Pivotal\")\n@has_request_variables\ndef api_pivotal_webhook(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:\n    subject = content = None\n    try:\n        subject, content = api_pivotal_webhook_v3(request, user_profile)\n    except Exception:\n        # Attempt to parse v5 JSON payload\n        subject, content = api_pivotal_webhook_v5(request, user_profile)\n\n    if not content:\n        return json_error(_(\"Unable to handle Pivotal payload\"))\n\n    check_send_webhook_message(request, user_profile, subject, content)\n    return json_success()\n", "path": "zerver/webhooks/pivotal/view.py"}], "after_files": [{"content": "\"\"\"Webhooks for external integrations.\"\"\"\nimport re\nfrom typing import Any, Dict, List, Optional, Tuple\n\nimport orjson\nfrom defusedxml.ElementTree import fromstring as xml_fromstring\nfrom django.http import HttpRequest, HttpResponse\nfrom django.utils.translation import gettext as _\n\nfrom zerver.decorator import webhook_view\nfrom zerver.lib.exceptions import UnsupportedWebhookEventType\nfrom zerver.lib.request import has_request_variables\nfrom zerver.lib.response import json_error, json_success\nfrom zerver.lib.webhooks.common import check_send_webhook_message\nfrom zerver.models import UserProfile\n\n\ndef api_pivotal_webhook_v3(request: HttpRequest, user_profile: UserProfile) -> Tuple[str, str]:\n    payload = xml_fromstring(request.body)\n\n    def get_text(attrs: List[str]) -> str:\n        start = payload\n        try:\n            for attr in attrs:\n                start = start.find(attr)\n            return start.text\n        except AttributeError:\n            return \"\"\n\n    event_type = payload.find(\"event_type\").text\n    description = payload.find(\"description\").text\n    project_id = payload.find(\"project_id\").text\n    story_id = get_text([\"stories\", \"story\", \"id\"])\n    # Ugh, the URL in the XML data is not a clickable URL that works for the user\n    # so we try to build one that the user can actually click on\n    url = f\"https://www.pivotaltracker.com/s/projects/{project_id}/stories/{story_id}\"\n\n    # Pivotal doesn't tell us the name of the story, but it's usually in the\n    # description in quotes as the first quoted string\n    name_re = re.compile(r'[^\"]+\"([^\"]+)\".*')\n    match = name_re.match(description)\n    if match and len(match.groups()):\n        name = match.group(1)\n    else:\n        name = \"Story changed\"  # Failed for an unknown reason, show something\n    more_info = f\" [(view)]({url}).\"\n\n    if event_type == \"story_update\":\n        subject = name\n        content = description + more_info\n    elif event_type == \"note_create\":\n        subject = \"Comment added\"\n        content = description + more_info\n    elif event_type == \"story_create\":\n        issue_desc = get_text([\"stories\", \"story\", \"description\"])\n        issue_type = get_text([\"stories\", \"story\", \"story_type\"])\n        issue_status = get_text([\"stories\", \"story\", \"current_state\"])\n        estimate = get_text([\"stories\", \"story\", \"estimate\"])\n        if estimate != \"\":\n            estimate = f\" worth {estimate} story points\"\n        subject = name\n        content = f\"{description} ({issue_status} {issue_type}{estimate}):\\n\\n~~~ quote\\n{issue_desc}\\n~~~\\n\\n{more_info}\"\n    return subject, content\n\n\nUNSUPPORTED_EVENT_TYPES = [\n    \"task_create_activity\",\n    \"comment_delete_activity\",\n    \"task_delete_activity\",\n    \"task_update_activity\",\n    \"story_move_from_project_activity\",\n    \"story_delete_activity\",\n    \"story_move_into_project_activity\",\n    \"epic_update_activity\",\n    \"label_create_activity\",\n]\n\n\ndef api_pivotal_webhook_v5(request: HttpRequest, user_profile: UserProfile) -> Tuple[str, str]:\n    payload = orjson.loads(request.body)\n\n    event_type = payload[\"kind\"]\n\n    project_name = payload[\"project\"][\"name\"]\n    project_id = payload[\"project\"][\"id\"]\n\n    primary_resources = payload[\"primary_resources\"][0]\n    story_url = primary_resources[\"url\"]\n    story_type = primary_resources.get(\"story_type\")\n    story_id = primary_resources[\"id\"]\n    story_name = primary_resources[\"name\"]\n\n    performed_by = payload.get(\"performed_by\", {}).get(\"name\", \"\")\n\n    story_info = f\"[{project_name}](https://www.pivotaltracker.com/s/projects/{project_id}): [{story_name}]({story_url})\"\n\n    changes = payload.get(\"changes\", [])\n\n    content = \"\"\n    subject = f\"#{story_id}: {story_name}\"\n\n    def extract_comment(change: Dict[str, Any]) -> Optional[str]:\n        if change.get(\"kind\") == \"comment\":\n            return change.get(\"new_values\", {}).get(\"text\", None)\n        return None\n\n    if event_type == \"story_update_activity\":\n        # Find the changed valued and build a message\n        content += f\"{performed_by} updated {story_info}:\\n\"\n        for change in changes:\n            old_values = change.get(\"original_values\", {})\n            new_values = change[\"new_values\"]\n\n            if \"current_state\" in old_values and \"current_state\" in new_values:\n                content += \"* state changed from **{}** to **{}**\\n\".format(\n                    old_values[\"current_state\"], new_values[\"current_state\"]\n                )\n            if \"estimate\" in old_values and \"estimate\" in new_values:\n                old_estimate = old_values.get(\"estimate\", None)\n                if old_estimate is None:\n                    estimate = \"is now\"\n                else:\n                    estimate = f\"changed from {old_estimate} to\"\n                new_estimate = new_values[\"estimate\"] if new_values[\"estimate\"] is not None else \"0\"\n                content += f\"* estimate {estimate} **{new_estimate} points**\\n\"\n            if \"story_type\" in old_values and \"story_type\" in new_values:\n                content += \"* type changed from **{}** to **{}**\\n\".format(\n                    old_values[\"story_type\"], new_values[\"story_type\"]\n                )\n\n            comment = extract_comment(change)\n            if comment is not None:\n                content += f\"* Comment added:\\n~~~quote\\n{comment}\\n~~~\\n\"\n\n    elif event_type == \"comment_create_activity\":\n        for change in changes:\n            comment = extract_comment(change)\n            if comment is not None:\n                content += (\n                    f\"{performed_by} added a comment to {story_info}:\\n~~~quote\\n{comment}\\n~~~\"\n                )\n    elif event_type == \"story_create_activity\":\n        content += f\"{performed_by} created {story_type}: {story_info}\\n\"\n        for change in changes:\n            new_values = change.get(\"new_values\", {})\n            if \"current_state\" in new_values:\n                content += \"* State is **{}**\\n\".format(new_values[\"current_state\"])\n            if \"description\" in new_values:\n                content += \"* Description is\\n\\n> {}\".format(new_values[\"description\"])\n    elif event_type == \"story_move_activity\":\n        content = f\"{performed_by} moved {story_info}\"\n        for change in changes:\n            old_values = change.get(\"original_values\", {})\n            new_values = change[\"new_values\"]\n            if \"current_state\" in old_values and \"current_state\" in new_values:\n                content += \" from **{}** to **{}**.\".format(\n                    old_values[\"current_state\"], new_values[\"current_state\"]\n                )\n    elif event_type in UNSUPPORTED_EVENT_TYPES:\n        # Known but unsupported Pivotal event types\n        pass\n    else:\n        raise UnsupportedWebhookEventType(event_type)\n\n    return subject, content\n\n\n@webhook_view(\"Pivotal\")\n@has_request_variables\ndef api_pivotal_webhook(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:\n    subject = content = None\n    try:\n        subject, content = api_pivotal_webhook_v3(request, user_profile)\n    except Exception:\n        # Attempt to parse v5 JSON payload\n        subject, content = api_pivotal_webhook_v5(request, user_profile)\n\n    if not content:\n        return json_error(_(\"Unable to handle Pivotal payload\"))\n\n    check_send_webhook_message(request, user_profile, subject, content)\n    return json_success()\n", "path": "zerver/webhooks/pivotal/view.py"}]}