Skip to content

Commit 3c81c0d

Browse files
committed
integrations: Add ClickUp integration.
Creates an incoming webhook integration for ClickUp. The main use case is getting notifications when new ClickUp items such as task, list, folder, space, goals are created, updated or deleted. Fixes zulip#26529.
1 parent 6af748f commit 3c81c0d

39 files changed

+1723
-0
lines changed
Loading
26.6 KB
Loading
27.6 KB
Loading
Lines changed: 1 addition & 0 deletions
Loading

zerver/lib/integrations.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
375375
WebhookIntegration("buildbot", ["continuous-integration"], display_name="Buildbot"),
376376
WebhookIntegration("canarytoken", ["monitoring"], display_name="Thinkst Canarytokens"),
377377
WebhookIntegration("circleci", ["continuous-integration"], display_name="CircleCI"),
378+
WebhookIntegration("clickup", ["project-management"], display_name="ClickUp"),
378379
WebhookIntegration("clubhouse", ["project-management"]),
379380
WebhookIntegration("codeship", ["continuous-integration", "deployment"]),
380381
WebhookIntegration("crashlytics", ["monitoring"]),
@@ -735,6 +736,7 @@ def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
735736
ScreenshotConfig("bitbucket_job_completed.json", image_name="001.png"),
736737
ScreenshotConfig("github_job_completed.json", image_name="002.png"),
737738
],
739+
"clickup": [ScreenshotConfig("task_moved.json")],
738740
"clubhouse": [ScreenshotConfig("story_create.json")],
739741
"codeship": [ScreenshotConfig("error_build.json")],
740742
"crashlytics": [ScreenshotConfig("issue_message.json")],

zerver/webhooks/clickup/__init__.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from enum import Enum
2+
from typing import List
3+
4+
5+
class ConstantVariable(Enum):
6+
@classmethod
7+
def as_list(cls) -> List[str]:
8+
return [item.value for item in cls]
9+
10+
11+
class EventItemType(ConstantVariable):
12+
TASK: str = "task"
13+
LIST: str = "list"
14+
FOLDER: str = "folder"
15+
GOAL: str = "goal"
16+
SPACE: str = "space"
17+
18+
19+
class EventAcion(ConstantVariable):
20+
CREATED: str = "Created"
21+
UPDATED: str = "Updated"
22+
DELETED: str = "Deleted"
23+
24+
25+
SIMPLE_FIELDS = ["priority", "status"]
26+
27+
28+
class SpecialFields(ConstantVariable):
29+
# Event with unique payload
30+
NAME: str = "name"
31+
ASSIGNEE: str = "assignee_add"
32+
COMMENT: str = "comment"
33+
DUE_DATE: str = "due_date"
34+
MOVED: str = "section_moved"
35+
TIME_ESTIMATE: str = "time_estimate"
36+
TIME_SPENT: str = "time_spent"
37+
38+
39+
SPAMMY_FIELDS = ["tag", "tag_removed", "assignee_rem"]
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import re
2+
from typing import Any, Dict, Optional, Union
3+
from urllib.parse import urljoin
4+
5+
import requests
6+
from django.utils.translation import gettext as _
7+
from typing_extensions import override
8+
9+
from zerver.lib.exceptions import ErrorCode, WebhookError
10+
from zerver.lib.outgoing_http import OutgoingSession
11+
from zerver.webhooks.clickup import EventItemType
12+
13+
14+
class APIUnavailableCallBackError(WebhookError):
15+
"""Intended as an exception for when an integration
16+
couldn't reach external API server when calling back
17+
from Zulip app.
18+
19+
Exception when callback request has timed out or received
20+
connection error.
21+
"""
22+
23+
code = ErrorCode.REQUEST_TIMEOUT
24+
http_status_code = 200
25+
data_fields = ["webhook_name"]
26+
27+
def __init__(self) -> None:
28+
super().__init__()
29+
30+
@staticmethod
31+
@override
32+
def msg_format() -> str:
33+
return _("{webhook_name} integration couldn't reach an external API service; ignoring")
34+
35+
36+
class BadRequestCallBackError(WebhookError):
37+
"""Intended as an exception for when an integration
38+
makes a bad request to external API server.
39+
40+
Exception when callback request has an invalid format.
41+
"""
42+
43+
code = ErrorCode.BAD_REQUEST
44+
http_status_code = 200
45+
data_fields = ["webhook_name", "error_detail"]
46+
47+
def __init__(self, error_detail: Optional[Union[str, int]]) -> None:
48+
super().__init__()
49+
self.error_detail = error_detail
50+
51+
@staticmethod
52+
@override
53+
def msg_format() -> str:
54+
return _(
55+
"{webhook_name} integration tries to make a bad outgoing request: {error_detail}; ignoring"
56+
)
57+
58+
59+
class ClickUpSession(OutgoingSession):
60+
def __init__(self, **kwargs: Any) -> None:
61+
super().__init__(role="clickup", timeout=5, **kwargs) # nocoverage
62+
63+
64+
def verify_url_path(path: str) -> bool:
65+
parts = path.split("/")
66+
if (len(parts) < 2) or (parts[0] not in EventItemType.as_list()) or (parts[1] == ""):
67+
return False
68+
pattern = r"^[a-zA-Z0-9_-]+$"
69+
match = re.match(pattern, parts[1])
70+
return match is not None and match.group() == parts[1]
71+
72+
73+
def make_clickup_request(path: str, api_key: str) -> Dict[str, Any]:
74+
if verify_url_path(path) is False:
75+
raise BadRequestCallBackError("Invalid path")
76+
headers: Dict[str, str] = {
77+
"Content-Type": "application/json",
78+
"Authorization": api_key,
79+
}
80+
81+
try:
82+
base_url = "https://api.clickup.com/api/v2/"
83+
api_endpoint = urljoin(base_url, path)
84+
response = ClickUpSession(headers=headers).get(
85+
api_endpoint,
86+
)
87+
response.raise_for_status()
88+
except (requests.ConnectionError, requests.Timeout):
89+
raise APIUnavailableCallBackError
90+
except requests.HTTPError as e:
91+
raise BadRequestCallBackError(e.response.status_code)
92+
93+
return response.json()
94+
95+
96+
def get_list(api_key: str, list_id: str) -> Dict[str, Any]:
97+
data = make_clickup_request(f"list/{list_id}", api_key)
98+
return data
99+
100+
101+
def get_task(api_key: str, task_id: str) -> Dict[str, Any]:
102+
data = make_clickup_request(f"task/{task_id}", api_key)
103+
return data
104+
105+
106+
def get_folder(api_key: str, folder_id: str) -> Dict[str, Any]:
107+
data = make_clickup_request(f"folder/{folder_id}", api_key)
108+
return data
109+
110+
111+
def get_goal(api_key: str, goal_id: str) -> Dict[str, Any]:
112+
data = make_clickup_request(f"goal/{goal_id}", api_key)
113+
return data
114+
115+
116+
def get_space(api_key: str, space_id: str) -> Dict[str, Any]:
117+
data = make_clickup_request(f"space/{space_id}", api_key)
118+
return data
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"id": "457",
3+
"name": "Lord Foldemort",
4+
"orderindex": 0,
5+
"override_statuses": false,
6+
"hidden": false,
7+
"space": {
8+
"id": "789",
9+
"name": "Space Name",
10+
"access": true
11+
},
12+
"task_count": "0",
13+
"lists": []
14+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"goal": {
3+
"id": "e53a033c-900e-462d-a849-4a216b06d930",
4+
"name": "hat-trick",
5+
"team_id": "512",
6+
"date_created": "1568044355026",
7+
"start_date": null,
8+
"due_date": "1568036964079",
9+
"description": "Updated Goal Description",
10+
"private": false,
11+
"archived": false,
12+
"creator": 183,
13+
"color": "#32a852",
14+
"pretty_id": "6",
15+
"multiple_owners": true,
16+
"folder_id": null,
17+
"members": [],
18+
"owners": [
19+
{
20+
"id": 182,
21+
"username": "Pieter CK",
22+
"email": "[email protected]",
23+
"color": "#7b68ee",
24+
"initials": "PK",
25+
"profilePicture": "https://attachments-public.clickup.com/profilePictures/182_abc.jpg"
26+
}
27+
],
28+
"key_results": [],
29+
"percent_completed": 0,
30+
"history": [],
31+
"pretty_url": "https://app.clickup.com/512/goals/6"
32+
}
33+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"id": "124",
3+
"name": "Listener",
4+
"orderindex": 1,
5+
"content": "Updated List Content",
6+
"status": {
7+
"status": "red",
8+
"color": "#e50000",
9+
"hide_label": true
10+
},
11+
"priority": {
12+
"priority": "high",
13+
"color": "#f50000"
14+
},
15+
"assignee": null,
16+
"due_date": "1567780450202",
17+
"due_date_time": true,
18+
"start_date": null,
19+
"start_date_time": null,
20+
"folder": {
21+
"id": "456",
22+
"name": "Folder Name",
23+
"hidden": false,
24+
"access": true
25+
},
26+
"space": {
27+
"id": "789",
28+
"name": "Space Name",
29+
"access": true
30+
},
31+
"inbound_address": "add.task.124.ac725f.31518a6a-05bb-4997-92a6-1dcfe2f527ca@tasks.clickup.com",
32+
"archived": false,
33+
"override_statuses": false,
34+
"statuses": [
35+
{
36+
"status": "to do",
37+
"orderindex": 0,
38+
"color": "#d3d3d3",
39+
"type": "open"
40+
},
41+
{
42+
"status": "complete",
43+
"orderindex": 1,
44+
"color": "#6bc950",
45+
"type": "closed"
46+
}
47+
],
48+
"permission_level": "create"
49+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"id": "790",
3+
"name": "the Milky Way",
4+
"private": false,
5+
"statuses": [
6+
{
7+
"status": "to do",
8+
"type": "open",
9+
"orderindex": 0,
10+
"color": "#d3d3d3"
11+
},
12+
{
13+
"status": "complete",
14+
"type": "closed",
15+
"orderindex": 1,
16+
"color": "#6bc950"
17+
}
18+
],
19+
"multiple_assignees": false,
20+
"features": {
21+
"due_dates": {
22+
"enabled": false,
23+
"start_date": false,
24+
"remap_due_dates": false,
25+
"remap_closed_due_date": false
26+
},
27+
"time_tracking": {
28+
"enabled": false
29+
},
30+
"tags": {
31+
"enabled": false
32+
},
33+
"time_estimates": {
34+
"enabled": false
35+
},
36+
"checklists": {
37+
"enabled": true
38+
},
39+
"custom_fields": {
40+
"enabled": true
41+
},
42+
"remap_dependencies": {
43+
"enabled": false
44+
},
45+
"dependency_warning": {
46+
"enabled": false
47+
},
48+
"portfolios": {
49+
"enabled": false
50+
}
51+
}
52+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"id": "string",
3+
"custom_id": "string",
4+
"custom_item_id": 0,
5+
"name": "Tanswer",
6+
"text_content": "string",
7+
"description": "string",
8+
"status": {
9+
"status": "in progress",
10+
"color": "#d3d3d3",
11+
"orderindex": 1,
12+
"type": "custom"
13+
},
14+
"orderindex": "string",
15+
"date_created": "string",
16+
"date_updated": "string",
17+
"date_closed": "string",
18+
"creator": {
19+
"id": 183,
20+
"username": "Pieter CK",
21+
"color": "#827718",
22+
"profilePicture": "https://attachments-public.clickup.com/profilePictures/183_abc.jpg"
23+
},
24+
"assignees": ["string"],
25+
"checklists": ["string"],
26+
"tags": ["string"],
27+
"parent": "string",
28+
"priority": "string",
29+
"due_date": "string",
30+
"start_date": "string",
31+
"time_estimate": "string",
32+
"time_spent": "string",
33+
"custom_fields": [
34+
{
35+
"id": "string",
36+
"name": "string",
37+
"type": "string",
38+
"type_config": {},
39+
"date_created": "string",
40+
"hide_from_guests": true,
41+
"value": {
42+
"id": 183,
43+
"username": "Pieter CK",
44+
"email": "[email protected]",
45+
"color": "#7b68ee",
46+
"initials": "PK",
47+
"profilePicture": null
48+
},
49+
"required": true
50+
}
51+
],
52+
"list": {
53+
"id": "123"
54+
},
55+
"folder": {
56+
"id": "456"
57+
},
58+
"space": {
59+
"id": "789"
60+
},
61+
"url": "https://app.clickup.com/XXXXXXXX/home",
62+
"markdown_description": "string"
63+
}

0 commit comments

Comments
 (0)