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

This commit is contained in:
upstream-mirror
2026-09-15 15:05:30 +05:00
commit 64898b2c79
8999 changed files with 1757358 additions and 0 deletions

View File

View File

@@ -0,0 +1,140 @@
# Zulip Freshdesk integration
See customer support interactions in Zulip with our Freshdesk
integration!
### Create Zulip bot for Freshdesk notifications
{start_tabs}
1. {!create-an-incoming-webhook.md!}
1. {!generate-webhook-url-basic.md!}
{end_tabs}
### Add notifications for new Freshdesk tickets
{start_tabs}
1. Go to your Freshdesk **Admin** page.
1. Under **Helpdesk Productivity**, select **Dispatch'r**, and then
select **New rule**.
1. Set **Rule Name** to a name of your choice, such as `Zulip`.
1. There isn't a shortcut to "always generate a notification on ticket
creation", so we'll have to fake it by picking two complementary
conditions: when the source **is email**, and when the source **is
not email**. Set up the **Conditions** for the new rule, like so:
![](/static/images/integrations/freshdesk/001.png)
1. Under **Actions**, set the **Select Action** dropdown to **Trigger
Webhook**.
1. Set **Request Type** to **POST**, and set **Callback URL** to the URL
[generated above][create-bot].
1. Toggle the **Requires Authentication** checkbox.
1. Set **Username** to the email of the bot [created above][create-bot],
and set **Password** to the bot's API key.
1. Set **Encoding** to **JSON**, and select the **Advanced** option.
Copy and paste the following JSON into the **Content** box:
```
{% raw %}
{"freshdesk_webhook":
{
"triggered_event":"{{triggered_event}}",
"ticket_id":"{{ticket.id}}",
"ticket_url":"{{ticket.url}}",
"ticket_type":"{{ticket.ticket_type}}",
"ticket_subject":"{{ticket.subject}}",
"ticket_description":"{{ticket.description}}",
"ticket_status":"{{ticket.status}}",
"ticket_priority":"{{ticket.priority}}",
"requester_name":"{{ticket.requester.name}}",
"requester_email":"{{ticket.requester.email}}"
}
}
{% endraw %}
```
1. Click **Save**.
{end_tabs}
### Get notifications for changes to existing Freshdesk tickets
{start_tabs}
1. Go to your Freshdesk **Admin** page.
1. Under **Helpdesk Productivity**, select **Observer**, and then select
**New rule**.
1. Set **Rule Name** to a name of your choice, such as `Zulip`.
1. Under **involves any of these events**, create new events as shown below:
![](/static/images/integrations/freshdesk/002.png)
1. Unfortunately, there isn't a shortcut for specifying "all tickets",
so we'll have to fake it by picking two complementary conditions:
when the source **is email**, and when the source **is not email**.
Under **on tickets with these properties**, create new conditions,
like so:
![](/static/images/integrations/freshdesk/003.png)
1. Under **perform these actions**, set the **Select Action** dropdown
to **Trigger Webhook**.
1. Set **Request Type** to **POST**, and set **Callback URL** to the URL
[generated above][create-bot].
1. Toggle the **Requires Authentication** checkbox.
1. Set **Username** to the email of the bot [created above][create-bot],
and set **Password** to the bot's API key.
1. Set **Encoding** to **JSON** and select the **Advanced** option.
Copy and paste the following JSON into the **Content** box:
```
{% raw %}
{"freshdesk_webhook":
{
"triggered_event":"{{triggered_event}}",
"ticket_id":"{{ticket.id}}",
"ticket_url":"{{ticket.url}}",
"ticket_type":"{{ticket.ticket_type}}",
"ticket_subject":"{{ticket.subject}}",
"ticket_description":"{{ticket.description}}",
"ticket_status":"{{ticket.status}}",
"ticket_priority":"{{ticket.priority}}",
"requester_name":"{{ticket.requester.name}}",
"requester_email":"{{ticket.requester.email}}"
}
}
{% endraw %}
```
1. Select **Save**.
{end_tabs}
{!congrats.md!}
![](/static/images/integrations/freshdesk/004.png)
### Related documentation
{!webhooks-url-specification.md!}
[create-bot]: #create-zulip-bot-for-freshdesk-notifications

View File

@@ -0,0 +1,14 @@
{"freshdesk_webhook":
{
"triggered_event":"{ticket_action:created}",
"ticket_id":"12",
"ticket_url":"http://test1234zzz.freshdesk.com/helpdesk/tickets/12",
"ticket_type":"Problem",
"ticket_subject":"Not enough ☃ guinea pigs",
"ticket_description":"<div>There are too many cat pictures on the internet ☃. We need more guinea pigs. Exhibit 1:</div>\n<div><br></div>\n<div></div>\n<div><img src=\"http://cdn.freshdesk.com/data/helpdesk/attachments/production/12744808/original/guinea_pig.png?1383958880\" class=\"inline-image\" data-id=\"12744808\" style=\"\"></div>\n<br>",
"ticket_status":"Open",
"ticket_priority":"Urgent",
"requester_name":"Requester ☃ Bob",
"requester_email":"requester-bob@example.com"
}
}

View File

@@ -0,0 +1,14 @@
{"freshdesk_webhook":
{
"triggered_event":"{priority:{from:3,to:1}}",
"ticket_id":"11",
"ticket_url":"http://test1234zzz.freshdesk.com/helpdesk/tickets/11",
"ticket_type":"Incident",
"ticket_subject":"Test ticket subject",
"ticket_description":"<div>Test ticket description.</div>",
"ticket_status":"Resolved",
"ticket_priority":"Low",
"requester_name":"Requester Bob",
"requester_email":"requester-bob@example.com"
}
}

View File

@@ -0,0 +1,14 @@
{"freshdesk_webhook":
{
"triggered_event":"{note_type:private}",
"ticket_id":"11",
"ticket_url":"http://test1234zzz.freshdesk.com/helpdesk/tickets/11",
"ticket_type":"Incident",
"ticket_subject":"Test ticket subject",
"ticket_description":"<div>Test ticket description.</div>",
"ticket_status":"Open",
"ticket_priority":"Medium",
"requester_name":"Requester Bob",
"requester_email":"requester-bob@example.com"
}
}

View File

@@ -0,0 +1,14 @@
{"freshdesk_webhook":
{
"triggered_event":"{note_type:public}",
"ticket_id":"11",
"ticket_url":"http://test1234zzz.freshdesk.com/helpdesk/tickets/11",
"ticket_type":"Incident",
"ticket_subject":"Test ticket subject",
"ticket_description":"<div>Test ticket description.</div>",
"ticket_status":"Open",
"ticket_priority":"Medium",
"requester_name":"Requester Bob",
"requester_email":"requester-bob@example.com"
}
}

View File

@@ -0,0 +1,14 @@
{"freshdesk_webhook":
{
"triggered_event":"{status:{from:4,to:6}}",
"ticket_id":"11",
"ticket_url":"http://test1234zzz.freshdesk.com/helpdesk/tickets/11",
"ticket_type":"Incident",
"ticket_subject":"Test ticket subject ☃",
"ticket_description":"<div>Test ticket description ☃.</div>",
"ticket_status":"Waiting on Customer",
"ticket_priority":"Low",
"requester_name":"Requester Bob",
"requester_email":"requester-bob@example.com"
}
}

View File

@@ -0,0 +1,13 @@
{"freshdesk_webhook":
{
"ticket_id":"11",
"ticket_url":"http://test1234zzz.freshdesk.com/helpdesk/tickets/11",
"ticket_type":"Incident",
"ticket_subject":"Test ticket subject ☃",
"ticket_description":"<div>Test ticket description ☃.</div>",
"ticket_status":"Waiting on Customer",
"ticket_priority":"Low",
"requester_name":"Requester Bob",
"requester_email":"requester-bob@example.com"
}
}

View File

@@ -0,0 +1,14 @@
{"freshdesk_webhook":
{
"triggered_event":"{ticket_action:created}",
"ticket_id":"11",
"ticket_url":"http://test1234zzz.freshdesk.com/helpdesk/tickets/11",
"ticket_type":"Incident",
"ticket_subject":"Test ticket subject ☃",
"ticket_description":"<div>Test ticket description ☃.</div>",
"ticket_status":"Pending",
"ticket_priority":"High",
"requester_name":"Requester ☃ Bob",
"requester_email":"requester-bob@example.com"
}
}

View File

@@ -0,0 +1,14 @@
{"freshdesk_webhook":
{
"triggered_event":"{unknown_event:{from:3,to:1}}",
"ticket_id":"11",
"ticket_url":"http://test1234zzz.freshdesk.com/helpdesk/tickets/11",
"ticket_type":"Incident",
"ticket_subject":"Test ticket subject",
"ticket_description":"<div>Test ticket description.</div>",
"ticket_status":"Resolved",
"ticket_priority":"Low",
"requester_name":"Requester Bob",
"requester_email":"requester-bob@example.com"
}
}

View File

@@ -0,0 +1,128 @@
from unittest.mock import MagicMock, patch
from zerver.lib.test_classes import WebhookTestCase
class FreshdeskHookTests(WebhookTestCase):
def test_ticket_creation(self) -> None:
"""
Messages are generated on ticket creation through Freshdesk's
"Dispatch'r" service.
"""
expected_topic_name = "#11: Test ticket subject ☃"
expected_message = """
Requester ☃ Bob <requester-bob@example.com> created [ticket #11](http://test1234zzz.freshdesk.com/helpdesk/tickets/11):
``` quote
Test ticket description ☃.
```
* **Type**: Incident
* **Priority**: High
* **Status**: Pending
""".strip()
self.api_channel_message(
self.test_user,
"ticket_created",
expected_topic_name,
expected_message,
content_type="application/x-www-form-urlencoded",
)
def test_status_change(self) -> None:
"""
Messages are generated when a ticket's status changes through
Freshdesk's "Observer" service.
"""
expected_topic_name = "#11: Test ticket subject ☃"
expected_message = """
Requester Bob <requester-bob@example.com> updated [ticket #11](http://test1234zzz.freshdesk.com/helpdesk/tickets/11):
* **Status**: Resolved -> Waiting on Customer
""".strip()
self.api_channel_message(
self.test_user,
"status_changed",
expected_topic_name,
expected_message,
content_type="application/x-www-form-urlencoded",
)
def test_priority_change(self) -> None:
"""
Messages are generated when a ticket's priority changes through
Freshdesk's "Observer" service.
"""
expected_topic_name = "#11: Test ticket subject"
expected_message = """
Requester Bob <requester-bob@example.com> updated [ticket #11](http://test1234zzz.freshdesk.com/helpdesk/tickets/11):
* **Priority**: High -> Low
""".strip()
self.api_channel_message(
self.test_user,
"priority_changed",
expected_topic_name,
expected_message,
content_type="application/x-www-form-urlencoded",
)
@patch("zerver.lib.webhooks.common.check_send_webhook_message")
def test_unknown_event_payload_ignore(self, check_send_webhook_message_mock: MagicMock) -> None:
"""
Ignore unknown event payloads.
"""
self.url = self.build_webhook_url()
payload = self.get_body("unknown_payload")
result = self.client_post(
self.url,
payload,
HTTP_AUTHORIZATION=self.encode_email(self.test_user.email),
content_type="application/x-www-form-urlencoded",
)
self.assertFalse(check_send_webhook_message_mock.called)
self.assert_json_success(result)
def note_change(self, fixture: str, note_type: str) -> None:
"""
Messages are generated when a note gets added to a ticket through
Freshdesk's "Observer" service.
"""
expected_topic_name = "#11: Test ticket subject"
expected_message = """
Requester Bob <requester-bob@example.com> added a {} note to \
[ticket #11](http://test1234zzz.freshdesk.com/helpdesk/tickets/11).
""".strip().format(note_type)
self.api_channel_message(
self.test_user,
fixture,
expected_topic_name,
expected_message,
content_type="application/x-www-form-urlencoded",
)
def test_private_note_change(self) -> None:
self.note_change("private_note", "private")
def test_public_note_change(self) -> None:
self.note_change("public_note", "public")
def test_inline_image(self) -> None:
"""
Freshdesk sends us descriptions as HTML, so we have to make the
descriptions Zulip Markdown-friendly while still doing our best to
preserve links and images.
"""
expected_topic_name = "#12: Not enough ☃ guinea pigs"
expected_message = """
Requester \u2603 Bob <requester-bob@example.com> created [ticket #12](http://test1234zzz.freshdesk.com/helpdesk/tickets/12):\n\n``` quote\nThere are too many cat pictures on the internet \u2603. We need more guinea pigs. Exhibit 1:\n\n[guinea_pig.png](http://cdn.freshdesk.com/data/helpdesk/attachments/production/12744808/original/guinea_pig.png?1383958880)\n```\n\n* **Type**: Problem\n* **Priority**: Urgent\n* **Status**: Open
""".strip()
self.api_channel_message(
self.test_user,
"inline_images",
expected_topic_name,
expected_message,
content_type="application/x-www-form-urlencoded",
)

View 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)