snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
158
zerver/webhooks/freshdesk/view.py
Normal file
158
zerver/webhooks/freshdesk/view.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""Webhooks for external integrations."""
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from zerver.decorator import authenticated_rest_api_view
|
||||
from zerver.lib.markdown.from_html import convert_html_to_markdown
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint
|
||||
from zerver.lib.validator import WildValue, check_string
|
||||
from zerver.lib.webhooks.common import check_send_webhook_message
|
||||
from zerver.models import UserProfile
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def property_name(property: str, index: int) -> str:
|
||||
"""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.
|
||||
"""
|
||||
statuses = [
|
||||
"",
|
||||
"",
|
||||
"Open",
|
||||
"Pending",
|
||||
"Resolved",
|
||||
"Closed",
|
||||
"Waiting on Customer",
|
||||
"Job Application",
|
||||
"Monthly",
|
||||
]
|
||||
priorities = ["", "Low", "Medium", "High", "Urgent"]
|
||||
|
||||
name = ""
|
||||
if property == "status":
|
||||
name = statuses[index] if index < len(statuses) else str(index)
|
||||
elif property == "priority":
|
||||
name = priorities[index] if index < len(priorities) else str(index)
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def parse_freshdesk_event(event_string: str) -> list[str]:
|
||||
"""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: WildValue, event_info: list[str]) -> str:
|
||||
"""There are public (visible to customers) and private note types."""
|
||||
note_type = event_info[1]
|
||||
content = NOTE_TEMPLATE.format(
|
||||
name=ticket["requester_name"].tame(check_string),
|
||||
email=ticket["requester_email"].tame(check_string),
|
||||
note_type=note_type,
|
||||
ticket_id=ticket["ticket_id"].tame(check_string),
|
||||
ticket_url=ticket["ticket_url"].tame(check_string),
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def format_freshdesk_property_change_message(ticket: WildValue, event_info: list[str]) -> str:
|
||||
"""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 = PROPERTY_CHANGE_TEMPLATE.format(
|
||||
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),
|
||||
property_name=event_info[0].capitalize(),
|
||||
old=event_info[1],
|
||||
new=event_info[2],
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def format_freshdesk_ticket_creation_message(ticket: WildValue) -> str:
|
||||
"""They send us the description as HTML."""
|
||||
cleaned_description = convert_html_to_markdown(ticket["ticket_description"].tame(check_string))
|
||||
content = TICKET_CREATION_TEMPLATE.format(
|
||||
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),
|
||||
description=cleaned_description,
|
||||
type=ticket["ticket_type"].tame(check_string),
|
||||
priority=ticket["ticket_priority"].tame(check_string),
|
||||
status=ticket["ticket_status"].tame(check_string),
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
@authenticated_rest_api_view(webhook_client_name="Freshdesk")
|
||||
@typed_endpoint
|
||||
def api_freshdesk_webhook(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
payload: JsonBodyPayload[WildValue],
|
||||
) -> HttpResponse:
|
||||
ticket = payload["freshdesk_webhook"]
|
||||
|
||||
topic_name = (
|
||||
f"#{ticket['ticket_id'].tame(check_string)}: {ticket['ticket_subject'].tame(check_string)}"
|
||||
)
|
||||
event_info = parse_freshdesk_event(ticket["triggered_event"].tame(check_string))
|
||||
|
||||
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(request)
|
||||
|
||||
check_send_webhook_message(request, user_profile, topic_name, content)
|
||||
return json_success(request)
|
||||
Reference in New Issue
Block a user