2016-06-05 23:03:26 +02:00
|
|
|
"""Webhooks for external integrations."""
|
2024-01-29 00:32:21 +01:00
|
|
|
|
2016-06-05 23:03:26 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
2016-05-25 15:02:02 +02:00
|
|
|
|
2017-10-31 04:25:48 +01:00
|
|
|
from zerver.decorator import authenticated_rest_api_view
|
2019-03-15 18:51:39 +01:00
|
|
|
from zerver.lib.email_notifications import convert_html_to_markdown
|
2020-07-28 08:43:29 +02:00
|
|
|
from zerver.lib.response import json_success
|
2023-09-27 19:01:31 +02:00
|
|
|
from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint
|
2023-08-12 09:34:31 +02:00
|
|
|
from zerver.lib.validator import WildValue, check_string
|
2018-03-16 22:53:50 +01:00
|
|
|
from zerver.lib.webhooks.common import check_send_webhook_message
|
2019-02-02 23:53:55 +01:00
|
|
|
from zerver.models import UserProfile
|
2016-03-13 15:06:54 +01:00
|
|
|
|
2019-05-07 03:44:33 +02:00
|
|
|
NOTE_TEMPLATE = "{name} <{email}> added a {note_type} note to [ticket #{ticket_id}]({ticket_url})."
|
|
|
|
PROPERTY_CHANGE_TEMPLATE = """
|
|
|
|
{name} <{email}> updated [ticket #{ticket_id}]({ticket_url}):
|
|
|
|
|
|
|
|
* **{property_name}**: {old} -> {new}
|
|
|
|
""".strip()
|
|
|
|
TICKET_CREATION_TEMPLATE = """
|
|
|
|
{name} <{email}> created [ticket #{ticket_id}]({ticket_url}):
|
|
|
|
|
|
|
|
``` quote
|
|
|
|
{description}
|
|
|
|
```
|
|
|
|
|
|
|
|
* **Type**: {type}
|
|
|
|
* **Priority**: {priority}
|
|
|
|
* **Status**: {status}
|
|
|
|
""".strip()
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-11-04 07:47:46 +01:00
|
|
|
def property_name(property: str, index: int) -> str:
|
2016-06-05 23:03:26 +02:00
|
|
|
"""The Freshdesk API is currently pretty broken: statuses are customizable
|
|
|
|
but the API will only tell you the number associated with the status, not
|
|
|
|
the name. While we engage the Freshdesk developers about exposing this
|
|
|
|
information through the API, since only FlightCar uses this integration,
|
|
|
|
hardcode their statuses.
|
|
|
|
"""
|
2021-02-12 08:19:30 +01:00
|
|
|
statuses = [
|
|
|
|
"",
|
|
|
|
"",
|
|
|
|
"Open",
|
|
|
|
"Pending",
|
|
|
|
"Resolved",
|
|
|
|
"Closed",
|
|
|
|
"Waiting on Customer",
|
|
|
|
"Job Application",
|
|
|
|
"Monthly",
|
|
|
|
]
|
2016-03-13 15:06:54 +01:00
|
|
|
priorities = ["", "Low", "Medium", "High", "Urgent"]
|
|
|
|
|
2018-09-25 19:28:05 +02:00
|
|
|
name = ""
|
2016-03-13 15:06:54 +01:00
|
|
|
if property == "status":
|
2018-09-25 19:28:05 +02:00
|
|
|
name = statuses[index] if index < len(statuses) else str(index)
|
2016-03-13 15:06:54 +01:00
|
|
|
elif property == "priority":
|
2018-09-25 19:28:05 +02:00
|
|
|
name = priorities[index] if index < len(priorities) else str(index)
|
|
|
|
|
|
|
|
return name
|
2016-03-13 15:06:54 +01:00
|
|
|
|
2016-06-05 23:03:26 +02:00
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
def parse_freshdesk_event(event_string: str) -> list[str]:
|
2016-06-05 23:03:26 +02:00
|
|
|
"""These are always of the form "{ticket_action:created}" or
|
|
|
|
"{status:{from:4,to:6}}". Note the lack of string quoting: this isn't
|
|
|
|
valid JSON so we have to parse it ourselves.
|
|
|
|
"""
|
2016-03-13 15:06:54 +01:00
|
|
|
data = event_string.replace("{", "").replace("}", "").replace(",", ":").split(":")
|
|
|
|
|
|
|
|
if len(data) == 2:
|
|
|
|
# This is a simple ticket action event, like
|
|
|
|
# {ticket_action:created}.
|
|
|
|
return data
|
|
|
|
else:
|
|
|
|
# This is a property change event, like {status:{from:4,to:6}}. Pull out
|
|
|
|
# the property, from, and to states.
|
|
|
|
property, _, from_state, _, to_state = data
|
2021-02-12 08:19:30 +01:00
|
|
|
return [
|
|
|
|
property,
|
|
|
|
property_name(property, int(from_state)),
|
|
|
|
property_name(property, int(to_state)),
|
|
|
|
]
|
2016-06-05 23:03:26 +02:00
|
|
|
|
2016-03-13 15:06:54 +01:00
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
def format_freshdesk_note_message(ticket: WildValue, event_info: list[str]) -> str:
|
2016-06-05 23:03:26 +02:00
|
|
|
"""There are public (visible to customers) and private note types."""
|
2016-03-13 15:06:54 +01:00
|
|
|
note_type = event_info[1]
|
2019-05-07 03:44:33 +02:00
|
|
|
content = NOTE_TEMPLATE.format(
|
2022-05-11 08:15:12 +02:00
|
|
|
name=ticket["requester_name"].tame(check_string),
|
|
|
|
email=ticket["requester_email"].tame(check_string),
|
2019-05-07 03:44:33 +02:00
|
|
|
note_type=note_type,
|
2022-05-11 08:15:12 +02:00
|
|
|
ticket_id=ticket["ticket_id"].tame(check_string),
|
|
|
|
ticket_url=ticket["ticket_url"].tame(check_string),
|
2019-05-07 03:44:33 +02:00
|
|
|
)
|
2016-03-13 15:06:54 +01:00
|
|
|
|
|
|
|
return content
|
|
|
|
|
2016-06-05 23:03:26 +02:00
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
def format_freshdesk_property_change_message(ticket: WildValue, event_info: list[str]) -> str:
|
2016-06-05 23:03:26 +02:00
|
|
|
"""Freshdesk will only tell us the first event to match our webhook
|
|
|
|
configuration, so if we change multiple properties, we only get the before
|
|
|
|
and after data for the first one.
|
|
|
|
"""
|
2019-05-07 03:44:33 +02:00
|
|
|
content = PROPERTY_CHANGE_TEMPLATE.format(
|
2022-05-11 08:15:12 +02:00
|
|
|
name=ticket["requester_name"].tame(check_string),
|
|
|
|
email=ticket["requester_email"].tame(check_string),
|
|
|
|
ticket_id=ticket["ticket_id"].tame(check_string),
|
|
|
|
ticket_url=ticket["ticket_url"].tame(check_string),
|
2019-05-07 03:44:33 +02:00
|
|
|
property_name=event_info[0].capitalize(),
|
|
|
|
old=event_info[1],
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
new=event_info[2],
|
2019-05-07 03:44:33 +02:00
|
|
|
)
|
2016-03-13 15:06:54 +01:00
|
|
|
|
|
|
|
return content
|
|
|
|
|
2016-06-05 23:03:26 +02:00
|
|
|
|
2022-05-11 08:15:12 +02:00
|
|
|
def format_freshdesk_ticket_creation_message(ticket: WildValue) -> str:
|
2016-06-05 23:03:26 +02:00
|
|
|
"""They send us the description as HTML."""
|
2022-05-11 08:15:12 +02:00
|
|
|
cleaned_description = convert_html_to_markdown(ticket["ticket_description"].tame(check_string))
|
2019-05-07 03:44:33 +02:00
|
|
|
content = TICKET_CREATION_TEMPLATE.format(
|
2022-05-11 08:15:12 +02:00
|
|
|
name=ticket["requester_name"].tame(check_string),
|
|
|
|
email=ticket["requester_email"].tame(check_string),
|
|
|
|
ticket_id=ticket["ticket_id"].tame(check_string),
|
|
|
|
ticket_url=ticket["ticket_url"].tame(check_string),
|
2019-05-07 03:44:33 +02:00
|
|
|
description=cleaned_description,
|
2022-05-11 08:15:12 +02:00
|
|
|
type=ticket["ticket_type"].tame(check_string),
|
|
|
|
priority=ticket["ticket_priority"].tame(check_string),
|
|
|
|
status=ticket["ticket_status"].tame(check_string),
|
2019-05-07 03:44:33 +02:00
|
|
|
)
|
2016-03-13 15:06:54 +01:00
|
|
|
|
|
|
|
return content
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2018-03-16 23:37:32 +01:00
|
|
|
@authenticated_rest_api_view(webhook_client_name="Freshdesk")
|
2023-08-12 09:34:31 +02:00
|
|
|
@typed_endpoint
|
2021-02-12 08:19:30 +01:00
|
|
|
def api_freshdesk_webhook(
|
|
|
|
request: HttpRequest,
|
|
|
|
user_profile: UserProfile,
|
2023-08-12 09:34:31 +02:00
|
|
|
*,
|
2023-09-27 19:01:31 +02:00
|
|
|
payload: JsonBodyPayload[WildValue],
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> HttpResponse:
|
2022-05-11 08:15:12 +02:00
|
|
|
ticket = payload["freshdesk_webhook"]
|
2016-03-13 15:06:54 +01:00
|
|
|
|
2024-01-17 15:53:30 +01:00
|
|
|
topic_name = (
|
2022-05-11 08:15:12 +02:00
|
|
|
f"#{ticket['ticket_id'].tame(check_string)}: {ticket['ticket_subject'].tame(check_string)}"
|
|
|
|
)
|
|
|
|
event_info = parse_freshdesk_event(ticket["triggered_event"].tame(check_string))
|
2016-03-13 15:06:54 +01:00
|
|
|
|
|
|
|
if event_info[1] == "created":
|
|
|
|
content = format_freshdesk_ticket_creation_message(ticket)
|
|
|
|
elif event_info[0] == "note_type":
|
|
|
|
content = format_freshdesk_note_message(ticket, event_info)
|
|
|
|
elif event_info[0] in ("status", "priority"):
|
|
|
|
content = format_freshdesk_property_change_message(ticket, event_info)
|
|
|
|
else:
|
|
|
|
# Not an event we know handle; do nothing.
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request)
|
2016-03-13 15:06:54 +01:00
|
|
|
|
2024-01-17 15:53:30 +01:00
|
|
|
check_send_webhook_message(request, user_profile, topic_name, content)
|
2022-01-31 13:44:02 +01:00
|
|
|
return json_success(request)
|