mirror of
https://github.com/zulip/zulip.git
synced 2025-11-05 06:23:38 +00:00
Add a basic Freshdesk webhook.
The Freshdesk API is bonkers, but we do the best we can with it to support notifications on ticket creation and ticket updates. (imported from commit 2023622b274ef83f4e1544d0df286fe2e68581b3)
This commit is contained in:
@@ -15,6 +15,7 @@ from zerver.views import send_message_backend
|
||||
from django.db.models import Q
|
||||
|
||||
from defusedxml.ElementTree import fromstring as xml_fromstring
|
||||
import html2text
|
||||
|
||||
import base64
|
||||
import logging
|
||||
@@ -581,3 +582,128 @@ def api_stash_webhook(request, user_profile, stream=REQ(default='')):
|
||||
check_send_message(user_profile, get_client("Stash"), "stream", [stream],
|
||||
subject, content)
|
||||
return json_success()
|
||||
|
||||
class TicketDict(dict):
|
||||
"""
|
||||
A helper class to turn a dictionary with ticket information into
|
||||
an object where each of the keys is an attribute for easy access.
|
||||
"""
|
||||
def __getattr__(self, field):
|
||||
if "_" in field:
|
||||
return self.get(field)
|
||||
else:
|
||||
return self.get("ticket_" + field)
|
||||
|
||||
def property_name(property, index):
|
||||
statuses = ["", "", "Open", "Pending", "Resolved", "Closed",
|
||||
"Waiting on Customer", "Waiting on Third Party"]
|
||||
priorities = ["", "Low", "Medium", "High", "Urgent"]
|
||||
|
||||
if property == "status":
|
||||
return statuses[index]
|
||||
elif property == "priority":
|
||||
return priorities[index]
|
||||
else:
|
||||
raise ValueError("Unknown property")
|
||||
|
||||
def parse_freshdesk_event(event_string):
|
||||
# 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.
|
||||
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
|
||||
return (property, property_name(property, int(from_state)),
|
||||
property_name(property, int(to_state)))
|
||||
|
||||
def format_freshdesk_note_message(ticket, event_info):
|
||||
# There are public (visible to customers) and private note types.
|
||||
note_type = event_info[1]
|
||||
content = "%s <%s> added a %s note to [ticket #%s](%s)." % (
|
||||
ticket.requester_name, ticket.requester_email, note_type,
|
||||
ticket.id, ticket.url)
|
||||
|
||||
return content
|
||||
|
||||
def format_freshdesk_property_change_message(ticket, event_info):
|
||||
# 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.
|
||||
content = "%s <%s> updated [ticket #%s](%s):\n\n" % (
|
||||
ticket.requester_name, ticket.requester_email, ticket.id, ticket.url)
|
||||
# Why not `"%s %s %s" % event_info`? Because the linter doesn't like it.
|
||||
content += "%s: **%s** => **%s**" % (
|
||||
event_info[0].capitalize(), event_info[1], event_info[2])
|
||||
|
||||
return content
|
||||
|
||||
def format_freshdesk_ticket_creation_message(ticket):
|
||||
# They send us the description as HTML.
|
||||
converter = html2text.HTML2Text()
|
||||
cleaned_description = converter.handle(ticket.description).strip()
|
||||
|
||||
content = "%s <%s> created [ticket #%s](%s):\n\n" % (
|
||||
ticket.requester_name, ticket.requester_email, ticket.id, ticket.url)
|
||||
content += """~~~ quote
|
||||
%s
|
||||
~~~\n
|
||||
""" % (cleaned_description,)
|
||||
content += "Type: **%s**\nPriority: **%s**\nStatus: **%s**" % (
|
||||
ticket.type, ticket.priority, ticket.status)
|
||||
|
||||
return content
|
||||
|
||||
@csrf_exempt
|
||||
@authenticated_rest_api_view
|
||||
@has_request_variables
|
||||
def api_freshdesk_webhook(request, user_profile, stream=REQ(default='')):
|
||||
try:
|
||||
payload = ujson.loads(request.body)
|
||||
ticket_data = payload["freshdesk_webhook"]
|
||||
except ValueError:
|
||||
return json_error("Malformed JSON input")
|
||||
|
||||
required_keys = [
|
||||
"triggered_event", "ticket_id", "ticket_url", "ticket_type",
|
||||
"ticket_subject", "ticket_description", "ticket_status",
|
||||
"ticket_priority", "requester_name", "requester_email",
|
||||
]
|
||||
|
||||
for key in required_keys:
|
||||
if not ticket_data.get(key):
|
||||
return json_error("Missing key %s in JSON" % (key,))
|
||||
|
||||
try:
|
||||
stream = request.GET['stream']
|
||||
except (AttributeError, KeyError):
|
||||
stream = 'freshdesk'
|
||||
|
||||
ticket = TicketDict(ticket_data)
|
||||
|
||||
subject = "#%s: %s" % (ticket.id, ticket.subject)
|
||||
|
||||
try:
|
||||
event_info = parse_freshdesk_event(ticket.triggered_event)
|
||||
except ValueError:
|
||||
return json_error("Malformed event %s" % (ticket.triggered_event,))
|
||||
|
||||
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.
|
||||
return json_success()
|
||||
|
||||
check_send_message(user_profile, get_client("Freshdesk"), "stream",
|
||||
[stream], subject, content)
|
||||
return json_success()
|
||||
|
||||
@@ -156,6 +156,7 @@ urlpatterns += patterns('zerver.views',
|
||||
url(r'^api/v1/external/bitbucket$', 'webhooks.api_bitbucket_webhook'),
|
||||
url(r'^api/v1/external/desk$', 'webhooks.api_deskdotcom_webhook'),
|
||||
url(r'^api/v1/external/stash$', 'webhooks.api_stash_webhook'),
|
||||
url(r'^api/v1/external/freshdesk$', 'webhooks.api_freshdesk_webhook'),
|
||||
|
||||
url(r'^user_uploads/(?P<realm_id>\d*)/(?P<filename>.*)', 'rest_dispatch',
|
||||
{'GET': 'get_uploaded_file'}),
|
||||
|
||||
Reference in New Issue
Block a user