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,46 @@
# Zulip Gong integration
Get notifications about completed Gong calls in Zulip!
{start_tabs}
1. {!create-an-incoming-webhook.md!}
1. {!generate-webhook-url-basic.md!}
1. From your Gong dashboard, navigate to **Admin center > Settings >
Ecosystem > Automation rules**. Then, click **+ Add rule** to create a
new automation rule for sending call data to Zulip.
1. Configure the [rule filters][gong-call-search] to specify which calls
should trigger the webhook, and click **Save**.
1. In the **Action** dropdown, **Fire webhook** should be selected by
default. Enter the webhook URL generated above, and select **URL
includes key** for the authentication method.
1. Enter a name (e.g., "Zulip") and description for the automation rule.
1. Test the automation by clicking **Test now** with the preselected call
data. Alternatively, you can choose to **Select a different call** for
the test message.
1. Enable the rule by toggling the **Rule Status** switch.
{end_tabs}
{!congrats.md!}
![](/static/images/integrations/gong/001.png)
### Related documentation
- [Gong's webhook documentation][gong-webhook-rule]
- [Filtering Gong calls][gong-call-search]
{!webhooks-url-specification.md!}
[gong-webhook-rule]: https://help.gong.io/docs/create-a-webhook-rule
[gong-call-search]: https://help.gong.io/docs/search-for-calls

View File

@@ -0,0 +1,299 @@
{
"isTest": false,
"callData": {
"metaData": {
"id": "5599332235511222779",
"url": "http://local.gong-it.net:8080/call?id=5599332235511222771",
"title": "Sales team meeting",
"scheduled": "2019-10-18T14:00:00-07:00",
"started": "2019-10-18T14:03:37.041977-07:00",
"duration": 2159,
"primaryUserId": "7744111777663220493",
"direction": "Conference",
"system": "Zoom",
"scope": "External",
"media": "Video",
"language": "eng"
},
"context": [
{
"system": "Salesforce",
"objects": [
{
"objectType": "Opportunity",
"objectId": "0061Q00000mkU1fQAE",
"fields": [
{
"name": "StageName",
"value": "Open Renewal"
},
{
"name": "Number_of_seats__c",
"value": 28
},
{
"name": "Amount",
"value": 50000
},
{
"name": "Probability",
"value": 85
},
{
"name": "CloseDate",
"value": "2019-12-30"
},
{
"name": "ARR_impact_rollup__c",
"value": "false"
},
{
"name": "Name",
"value": "Acme - Renewal (28)"
},
{
"name": "Type",
"value": "Renewal"
},
{
"name": "Primary_contact_persona__c",
"value": "Manager of Sales"
},
{
"name": "ARR_future_impact__c",
"value": "false"
},
{
"name": "MRR__c",
"value": 4166.67
},
{
"name": "Must_Win__c",
"value": "false"
}
]
},
{
"objectType": "Account",
"objectId": "0011630000vmLBaEEN",
"fields": [
{
"name": "Industry",
"value": "Internet Software & Services"
},
{
"name": "Acme2__c",
"value": 1121
},
{
"name": "ARR__c",
"value": 25166.67
},
{
"name": "CS_Tier__c",
"value": "3. Gold"
},
{
"name": "Account_Behavior_Score__c",
"value": 5150
},
{
"name": "Website",
"value": "acme.com"
},
{
"name": "Sales_Team_Size__c",
"value": "232"
},
{
"name": "Score__c",
"value": 120
},
{
"name": "Size_Category__c",
"value": "2. Enterprise"
},
{
"name": "Name",
"value": "Acme"
}
]
}
]
}
],
"parties": [
{
"id": "7409609343412403343",
"emailAddress": "bwilliams@example.com",
"name": "Bo Williams",
"title": "Sales Enablement Manager - Sales Development",
"speakerId": "1177336688551133887",
"context": [
{
"system": "Salesforce",
"objects": [
{
"objectType": "Contact",
"objectId": "0331Z0000571AdPQDU",
"fields": [
{
"name": "AccountOwnerId__c",
"value": "0551Z00000KdYa3"
},
{
"name": "Account_Name__c",
"value": "Acme"
},
{
"name": "Account_Status__c",
"value": "Closed Won ARR: $34166.67"
}
]
}
]
}
],
"affiliation": "External",
"methods": [
"Invitee",
"Attendee"
]
},
{
"id": "9048546196233268852",
"emailAddress": "klin@example.com",
"name": "Kevin Lin",
"title": "Customer Success Manager",
"userId": "3022101605611220443",
"speakerId": "7265406288145283521",
"context": [
{
"system": "Salesforce",
"objects": [
{
"objectType": "User",
"objectId": "00557000007AJB1DDO",
"fields": [
{
"name": "PlayGongVoicePrompt__c",
"value": "false"
}
]
}
]
}
],
"affiliation": "Internal",
"methods": [
"Attendee"
]
}
],
"content": {
"trackers": [
{
"name": "Products",
"count": 1
},
{
"name": "Renewal (positive)",
"count": 7
},
{
"name": "Competitors",
"count": 1,
"phrases": [
{
"count": 1,
"phrase": "Zlymbo"
}
]
},
{
"name": "CRM",
"count": 3,
"phrases": [
{
"count": 3,
"phrase": "Salesforce"
}
]
}
],
"topics": [
{
"name": "Integrations",
"duration": 517
},
{
"name": "Differentiation",
"duration": 203
},
{
"name": "Customer Success",
"duration": 0
}
]
},
"interaction": {
"speakers": [
{
"id": "9048546196233268852",
"userId": "7041101707613290443",
"talkTime": 1032
}
],
"interactionStats": [
{
"name": "Talk Ratio",
"value": 0.51
},
{
"name": "Longest Monologue",
"value": 150
},
{
"name": "Longest Customer Story",
"value": 107
},
{
"name": "Interactivity",
"value": 5.07
},
{
"name": "Patience",
"value": 0.86
}
],
"video": [
{
"name": "Browser",
"duration": 0
},
{
"name": "Presentation",
"duration": 0
},
{
"name": "Webcam",
"duration": 1091.83
}
]
},
"collaboration": {
"publicComments": [
{
"id": "2255211009900224213",
"audioStartTime": 1800.8911,
"audioEndTime": 1800.8911,
"commenterUserId": "7041101707613290443",
"comment": "Might be helpful to loop us in on Outreach issue with Outreach team",
"posted": "2019-10-19T00:34:39.113071+03:00",
"inReplyTo": null,
"duringCall": true
}
]
}
}
}

View File

@@ -0,0 +1,362 @@
import orjson
from zerver.lib.test_classes import WebhookTestCase
from zerver.lib.validator import wrap_wild_value
from zerver.webhooks.fixtureless_integrations import BO_EMAIL, BO_NAME, KEVIN_EMAIL, KEVIN_NAME
from zerver.webhooks.gong.view import (
format_duration_to_string,
get_participants,
get_public_comments,
get_topics,
get_trackers,
)
class GongHookTests(WebhookTestCase):
CALL_TITLE = "Sales team meeting"
EXPECTED_TOPIC = f"Gong call: {CALL_TITLE}"
MESSAGE_INTRO = "**[{title}](http://local.gong-it.net:8080/call?id=5599332235511222771)** started at <time:2019-10-18T14:03:37-07:00> and lasted for 36 minutes."
PARTICIPANTS_SECTION = (
"**Participants**:\n"
f"* {BO_NAME}: Sales Enablement Manager - Sales Development ({BO_EMAIL})\n"
f"* {KEVIN_NAME}: Customer Success Manager ({KEVIN_EMAIL})"
)
TRACKERS_SECTION = (
"**Top Trackers (by count)**:\n"
"* Renewal (positive) (7)\n"
"* CRM (3): Salesforce (3)\n"
"* Products (1)"
)
TOPICS_SECTION = (
"**Topics**: Integrations (9 minutes), Differentiation (3 minutes), Customer Success."
)
EXPECTED_CALL_MESSAGE = (
f"{MESSAGE_INTRO}\n\n{PARTICIPANTS_SECTION}\n\n{TRACKERS_SECTION}\n\n{TOPICS_SECTION}"
)
def test_gong_normal_call_payload(self) -> None:
self.check_webhook(
"call_completed",
self.EXPECTED_TOPIC,
self.EXPECTED_CALL_MESSAGE.format(title=self.CALL_TITLE),
content_type="application/json",
)
def test_gong_test_call(self) -> None:
self.subscribe(self.test_user, self.channel_name)
payload = orjson.loads(self.get_body("call_completed"))
payload["isTest"] = True
msg = self.send_webhook_payload(
self.test_user,
self.url,
orjson.dumps(payload).decode(),
content_type="application/json",
)
self.assert_channel_message(
message=msg,
channel_name=self.channel_name,
topic_name="Gong Test",
content=f"Gong webhook has been successfully configured.\n\n{self.EXPECTED_CALL_MESSAGE.format(title=self.CALL_TITLE)}",
)
def test_call_without_title(self) -> None:
self.subscribe(self.test_user, self.channel_name)
payload = orjson.loads(self.get_body("call_completed"))
payload["callData"]["metaData"].pop("title")
msg = self.send_webhook_payload(
self.test_user,
self.url,
orjson.dumps(payload).decode(),
content_type="application/json",
)
self.assert_channel_message(
message=msg,
channel_name=self.channel_name,
topic_name="Gong call (ID: 5599332235511222779)",
content=self.EXPECTED_CALL_MESSAGE.format(title="Gong call"),
)
def test_call_without_participants(self) -> None:
self.subscribe(self.test_user, self.channel_name)
payload = orjson.loads(self.get_body("call_completed"))
payload["callData"]["parties"] = []
msg = self.send_webhook_payload(
self.test_user,
self.url,
orjson.dumps(payload).decode(),
content_type="application/json",
)
intro = self.MESSAGE_INTRO.format(title=self.CALL_TITLE)
self.assert_channel_message(
message=msg,
channel_name=self.channel_name,
topic_name=self.EXPECTED_TOPIC,
content=f"{intro}\n\n{self.TRACKERS_SECTION}\n\n{self.TOPICS_SECTION}",
)
def test_call_without_trackers(self) -> None:
self.subscribe(self.test_user, self.channel_name)
payload = orjson.loads(self.get_body("call_completed"))
payload["callData"]["content"].pop("trackers")
msg = self.send_webhook_payload(
self.test_user,
self.url,
orjson.dumps(payload).decode(),
content_type="application/json",
)
intro = self.MESSAGE_INTRO.format(title=self.CALL_TITLE)
self.assert_channel_message(
message=msg,
channel_name=self.channel_name,
topic_name=self.EXPECTED_TOPIC,
content=f"{intro}\n\n{self.PARTICIPANTS_SECTION}\n\n{self.TOPICS_SECTION}",
)
def test_call_without_topics(self) -> None:
self.subscribe(self.test_user, self.channel_name)
payload = orjson.loads(self.get_body("call_completed"))
payload["callData"]["content"].pop("topics")
msg = self.send_webhook_payload(
self.test_user,
self.url,
orjson.dumps(payload).decode(),
content_type="application/json",
)
intro = self.MESSAGE_INTRO.format(title=self.CALL_TITLE)
self.assert_channel_message(
message=msg,
channel_name=self.channel_name,
topic_name=self.EXPECTED_TOPIC,
content=f"{intro}\n\n{self.PARTICIPANTS_SECTION}\n\n{self.TRACKERS_SECTION}",
)
def test_call_with_top_trackers_disabled(self) -> None:
self.url = self.build_webhook_url(include_trackers="false")
intro = self.MESSAGE_INTRO.format(title=self.CALL_TITLE)
self.check_webhook(
"call_completed",
self.EXPECTED_TOPIC,
f"{intro}\n\n{self.PARTICIPANTS_SECTION}\n\n{self.TOPICS_SECTION}",
content_type="application/json",
)
def test_call_with_top_topics_disabled(self) -> None:
self.url = self.build_webhook_url(include_topics="false")
intro = self.MESSAGE_INTRO.format(title=self.CALL_TITLE)
self.check_webhook(
"call_completed",
self.EXPECTED_TOPIC,
f"{intro}\n\n{self.PARTICIPANTS_SECTION}\n\n{self.TRACKERS_SECTION}",
content_type="application/json",
)
def test_call_with_participants_disabled(self) -> None:
self.url = self.build_webhook_url(include_participants="false")
intro = self.MESSAGE_INTRO.format(title=self.CALL_TITLE)
self.check_webhook(
"call_completed",
self.EXPECTED_TOPIC,
f"{intro}\n\n{self.TRACKERS_SECTION}\n\n{self.TOPICS_SECTION}",
content_type="application/json",
)
def test_call_with_participant_contacts_disabled(self) -> None:
self.url = self.build_webhook_url(include_participant_contacts="false")
intro = self.MESSAGE_INTRO.format(title=self.CALL_TITLE)
participants = (
"**Participants**:\n"
f"* {BO_NAME}: Sales Enablement Manager - Sales Development\n"
f"* {KEVIN_NAME}: Customer Success Manager"
)
self.check_webhook(
"call_completed",
self.EXPECTED_TOPIC,
f"{intro}\n\n{participants}\n\n{self.TRACKERS_SECTION}\n\n{self.TOPICS_SECTION}",
content_type="application/json",
)
def test_call_with_participants_off_but_contacts_on(self) -> None:
self.url = self.build_webhook_url(
include_participants="false", include_participant_contacts="true"
)
intro = self.MESSAGE_INTRO.format(title=self.CALL_TITLE)
self.check_webhook(
"call_completed",
self.EXPECTED_TOPIC,
f"{intro}\n\n{self.TRACKERS_SECTION}\n\n{self.TOPICS_SECTION}",
content_type="application/json",
)
def test_call_with_public_comments_enabled(self) -> None:
self.url = self.build_webhook_url(include_public_comments="true")
comments = (
"**Comments**:\n* Might be helpful to loop us in on Outreach issue with Outreach team"
)
self.check_webhook(
"call_completed",
self.EXPECTED_TOPIC,
f"{self.EXPECTED_CALL_MESSAGE.format(title=self.CALL_TITLE)}\n\n{comments}",
content_type="application/json",
)
def test_get_participants(self) -> None:
name = KEVIN_NAME
title = "Customer Success Manager"
email = KEVIN_EMAIL
phone = "5556543210"
test_cases = [
([{"name": name}], f"* {name}"),
([{"name": name, "title": title}], f"* {name}: {title}"),
([{"name": name, "emailAddress": email}], f"* {name} ({email})"),
([{"name": name, "phoneNumber": phone}], f"* {name} ({phone})"),
(
[{"name": name, "title": title, "emailAddress": email}],
f"* {name}: {title} ({email})",
),
(
[{"name": name, "title": title, "phoneNumber": phone}],
f"* {name}: {title} ({phone})",
),
(
[{"name": name, "emailAddress": email, "phoneNumber": phone}],
f"* {name} ({email}, {phone})",
),
(
[{"name": name, "title": title, "emailAddress": email, "phoneNumber": phone}],
f"* {name}: {title} ({email}, {phone})",
),
([{"emailAddress": email}], f"* {email}"),
([{"phoneNumber": phone}], f"* {phone}"),
([{"emailAddress": email, "phoneNumber": phone}], f"* {email} ({phone})"),
(
[{"title": title, "emailAddress": email, "phoneNumber": phone}],
f"* {email}: {title} ({phone})",
),
([{"affiliation": "External"}], "* 1 participant"),
(
[{"affiliation": "External"}] * 3,
"* 3 participants",
),
(
[{"name": f"Person {i}"} for i in range(6)],
"* Person 0\n* Person 1\n* Person 2\n* Person 3\n* Person 4\n* and 1 more participant",
),
(
[{"name": f"Person {i}"} for i in range(7)],
"* Person 0\n* Person 1\n* Person 2\n* Person 3\n* Person 4\n* and 2 more participants",
),
(
[{"name": "Alice"}, {"name": "Bob"}] + [{"affiliation": "External"}] * 2,
"* Alice\n* Bob\n* and 2 more participants",
),
(
[{"name": f"Person {i}"} for i in range(6)] + [{"affiliation": "External"}] * 2,
"* Person 0\n* Person 1\n* Person 2\n* Person 3\n* Person 4\n* and 3 more participants",
),
([], ""),
]
for parties, expected_output in test_cases:
with self.subTest(parties=parties):
payload = wrap_wild_value("parties", parties)
self.assertEqual(get_participants(payload, True), expected_output)
no_contacts_cases = [
([{"name": name, "title": title, "emailAddress": email}], f"* {name}: {title}"),
([{"name": name, "emailAddress": email, "phoneNumber": phone}], f"* {name}"),
(
[{"name": "Alice"}, {"emailAddress": email}, {"phoneNumber": phone}],
"* Alice\n* and 2 more participants",
),
([{"title": title, "emailAddress": email}], "* 1 participant"),
]
for parties, expected_output in no_contacts_cases:
with self.subTest(parties=parties, include_participant_contacts=False):
payload = wrap_wild_value("parties", parties)
self.assertEqual(
get_participants(payload, False),
expected_output,
)
def test_get_trackers(self) -> None:
payload = wrap_wild_value(
"trackers",
[
{"name": "Products", "count": 1},
{"name": "Renewal (positive)", "count": 7},
{"name": "ERP", "count": 0},
{
"name": "Pricing",
"count": 5,
"phrases": [
{"count": 2, "phrase": "budget"},
{"count": 3, "phrase": "discount"},
],
},
{"name": "CRM", "count": 3, "phrases": [{"count": 3, "phrase": "Salesforce"}]},
],
)
self.assertEqual(
get_trackers(payload),
"* Renewal (positive) (7)\n"
"* Pricing (5): discount (3), budget (2)\n"
"* CRM (3): Salesforce (3)",
)
self.assertEqual(get_trackers(wrap_wild_value("trackers", [])), "")
def test_get_topics(self) -> None:
topics = [
{"name": "Small talk", "duration": 0},
{"name": "Pricing", "duration": 180},
{"name": "Next steps", "duration": 45},
{"name": "Revenue", "duration": 0},
]
self.assertEqual(
get_topics(wrap_wild_value("topics", topics)),
"Pricing (3 minutes), Next steps (45 seconds), Small talk and 1 more topic.",
)
topics.append({"name": "Wrap-up", "duration": 0})
self.assertEqual(
get_topics(wrap_wild_value("topics", topics)),
"Pricing (3 minutes), Next steps (45 seconds), Small talk and 2 more topics.",
)
self.assertEqual(get_topics(wrap_wild_value("topics", [])), "")
def test_get_public_comments(self) -> None:
payload = wrap_wild_value(
"publicComments",
[
{"comment": "Third", "posted": "2019-10-19T03:00:00+00:00"},
{"comment": "First", "posted": "2019-10-19T01:00:00+00:00"},
{"comment": "Second", "posted": "2019-10-19T02:00:00+00:00"},
{"comment": "Fourth", "posted": "2019-10-19T04:00:00+00:00"},
],
)
self.assertEqual(
get_public_comments(payload),
"* Fourth\n* Third\n* Second\n* and 1 more",
)
self.assertEqual(get_public_comments(wrap_wild_value("publicComments", [])), "")
def test_format_duration_to_string(self) -> None:
test_cases = [
(0, "0 seconds"),
(1, "1 second"),
(45, "45 seconds"),
(59, "59 seconds"),
(60, "1 minute"),
(3599, "1 hour"),
(3600, "1 hour"),
(3645, "1 hour 1 minute"),
(3661, "1 hour 1 minute"),
(7200, "2 hours"),
(7320, "2 hours 2 minutes"),
]
for duration, expected_output in test_cases:
with self.subTest(duration=duration):
self.assertEqual(format_duration_to_string(duration), expected_output)

View File

@@ -0,0 +1,248 @@
from django.http import HttpRequest, HttpResponse
from pydantic import Json
from zerver.decorator import webhook_view
from zerver.lib.response import json_success
from zerver.lib.timestamp import datetime_to_global_time
from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint
from zerver.lib.validator import (
WildValue,
check_bool,
check_int,
check_iso_datetime,
check_none_or,
check_string,
check_url,
)
from zerver.lib.webhooks.common import check_send_webhook_message, get_setup_webhook_message
from zerver.models import UserProfile
TOPIC_TEMPLATE = "Gong call: {call_title}"
MESSAGE_TEMPLATE = "**[{call_title}]({call_url})** started at {time} and lasted for {duration}."
SECTION_TEMPLATE = "**{heading}**:\n{body}"
TOPICS_TEMPLATE = "**Topics**: {topics}"
TOP_CONTENT_LIMIT = 3
PARTICIPANTS_LIMIT = 5
COMMENTS_LIMIT = 3
class Helper:
def __init__(
self,
payload: WildValue,
include_trackers: bool,
include_topics: bool,
include_participants: bool,
include_participant_contacts: bool,
include_public_comments: bool,
) -> None:
self.payload = payload
self.include_trackers = include_trackers
self.include_topics = include_topics
self.include_participants = include_participants
self.include_participant_contacts = include_participant_contacts
self.include_public_comments = include_public_comments
def format_duration_to_string(duration: int) -> str:
# The duration value is in seconds.
if duration < 60:
return f"{duration} {'second' if duration == 1 else 'seconds'}"
total_minutes = (duration + 30) // 60
hours, minutes = divmod(total_minutes, 60)
hour_word = "hour" if hours == 1 else "hours"
minute_word = "minute" if minutes == 1 else "minutes"
if hours > 0 and minutes > 0:
return f"{hours} {hour_word} {minutes} {minute_word}"
if hours > 0:
return f"{hours} {hour_word}"
return f"{minutes} {minute_word}"
def get_participants(parties_payload: WildValue, include_participant_contacts: bool) -> str:
# Gong may omit a party's name, email, and phone for unidentified callers
# (https://help.gong.io/docs/uploading-calls-from-a-non-integrated-telephony-system).
labels: list[str] = []
unidentified = 0
for party in parties_payload:
if len(labels) == PARTICIPANTS_LIMIT:
break
name = party.get("name")
contact_parts = (
[
party[key].tame(check_string)
for key in ("emailAddress", "phoneNumber")
if party.get(key)
]
if include_participant_contacts
else []
)
label = name.tame(check_string) if name else (contact_parts[0] if contact_parts else None)
if label is None:
unidentified += 1
continue
title = party.get("title")
title_part = f": {title.tame(check_string)}" if title else ""
details = [c for c in contact_parts if c != label]
contact_part = f" ({', '.join(details)})" if details else ""
labels.append(f"* {label}{title_part}{contact_part}")
if not labels:
if unidentified:
noun = "participant" if unidentified == 1 else "participants"
return f"* {unidentified} {noun}"
return ""
if additional_participants := len(parties_payload) - len(labels):
noun = "participant" if additional_participants == 1 else "participants"
labels.append(f"* and {additional_participants} more {noun}")
return "\n".join(labels)
def get_trackers(trackers_payload: WildValue) -> str:
trackers = []
for tracker in trackers_payload:
if (count := tracker["count"].tame(check_int)) == 0:
continue
trackers.append((tracker["name"].tame(check_string), count, tracker.get("phrases")))
trackers.sort(key=lambda tracker: tracker[1], reverse=True)
tracker_lines = []
for name, count, phrases in trackers[:TOP_CONTENT_LIMIT]:
line = f"* {name} ({count})"
if phrases:
sorted_phrases = sorted(
(
(phrase["phrase"].tame(check_string), phrase["count"].tame(check_int))
for phrase in phrases
),
key=lambda phrase: phrase[1],
reverse=True,
)
phrase_text = ", ".join(f"{text} ({n})" for text, n in sorted_phrases)
line += f": {phrase_text}"
tracker_lines.append(line)
return "\n".join(tracker_lines)
def get_topics(topics_payload: WildValue) -> str:
topics = [
(topic["name"].tame(check_string), topic["duration"].tame(check_int))
for topic in topics_payload
]
topics.sort(key=lambda topic: topic[1], reverse=True)
lines = []
for name, duration in topics[:TOP_CONTENT_LIMIT]:
if duration > 0:
lines.append(f"{name} ({format_duration_to_string(duration)})")
else:
lines.append(f"{name}")
topics_line = ", ".join(lines) if lines else ""
if (extra_topics := len(topics) - TOP_CONTENT_LIMIT) > 0:
topics_line += f" and {extra_topics} more {'topic' if extra_topics == 1 else 'topics'}"
return topics_line + "." if topics_line else ""
def get_public_comments(comments_payload: WildValue) -> str:
comments = sorted(
comments_payload,
key=lambda comment: comment["posted"].tame(check_iso_datetime),
reverse=True,
)
lines = []
for comment in comments[:COMMENTS_LIMIT]:
text = comment["comment"].tame(check_string)
lines.append(f"* {text}")
additional_comments = len(comments) - len(lines)
if additional_comments:
lines.append(f"* and {additional_comments} more")
return "\n".join(lines)
def handle_test_message(helper: Helper) -> tuple[str, str]:
# Gong's "Test now" sends a real, already-processed call, so render the
# actual message and prefix it with the standard setup confirmation.
_, body = handle_call_processed_message(helper)
return (
"Gong Test",
f"{get_setup_webhook_message('Gong')}\n\n{body}",
)
def handle_call_processed_message(helper: Helper) -> tuple[str, str]:
call_data = helper.payload["callData"]
meta_data = call_data["metaData"]
# call_title can be an optional field and can be omitted if not present.
call_title = meta_data.get("title").tame(check_none_or(check_string))
topic = (
TOPIC_TEMPLATE.format(call_title=call_title)
if call_title
else f"Gong call (ID: {meta_data['id'].tame(check_string)})"
)
body = MESSAGE_TEMPLATE.format(
call_title=call_title or "Gong call",
call_url=meta_data["url"].tame(check_url),
time=datetime_to_global_time(meta_data["started"].tame(check_iso_datetime)),
duration=format_duration_to_string(meta_data["duration"].tame(check_int)),
)
if helper.include_participants and (
participants := get_participants(call_data["parties"], helper.include_participant_contacts)
):
body += "\n\n" + SECTION_TEMPLATE.format(heading="Participants", body=participants)
if content := call_data.get("content"):
if (
helper.include_trackers
and "trackers" in content
and (top_trackers := get_trackers(content["trackers"]))
):
body += "\n\n" + SECTION_TEMPLATE.format(
heading="Top Trackers (by count)", body=top_trackers
)
if (
helper.include_topics
and "topics" in content
and (topics := get_topics(content["topics"]))
):
body += "\n\n" + TOPICS_TEMPLATE.format(topics=topics)
if (
helper.include_public_comments
and (public_comments_list := call_data["collaboration"].get("publicComments"))
and (public_comments := get_public_comments(public_comments_list))
):
body += "\n\n" + SECTION_TEMPLATE.format(heading="Comments", body=public_comments)
return topic, body
@webhook_view("Gong")
@typed_endpoint
def api_gong_webhook(
request: HttpRequest,
user_profile: UserProfile,
*,
payload: JsonBodyPayload[WildValue],
include_trackers: Json[bool] = True,
include_topics: Json[bool] = True,
include_participants: Json[bool] = True,
include_participant_contacts: Json[bool] = True,
include_public_comments: Json[bool] = False,
) -> HttpResponse:
helper = Helper(
payload,
include_trackers,
include_topics,
include_participants,
include_participant_contacts,
include_public_comments,
)
if payload.get("isTest").tame(check_bool):
topic, body = handle_test_message(helper)
else:
topic, body = handle_call_processed_message(helper)
check_send_webhook_message(request, user_profile, topic, body)
return json_success(request)