forked from zulip/zulip
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
38 changed files
with
1,681 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
from enum import Enum | ||
from typing import List | ||
|
||
|
||
class ConstantVariable(Enum): | ||
@classmethod | ||
def as_list(cls) -> List[str]: | ||
return [item.value for item in cls] | ||
|
||
|
||
class EventItemType(ConstantVariable): | ||
TASK: str = "task" | ||
LIST: str = "list" | ||
FOLDER: str = "folder" | ||
GOAL: str = "goal" | ||
SPACE: str = "space" | ||
|
||
|
||
class EventAcion(ConstantVariable): | ||
CREATED: str = "Created" | ||
UPDATED: str = "Updated" | ||
DELETED: str = "Deleted" | ||
|
||
|
||
class SimpleFields(ConstantVariable): | ||
# Events with identical payload format | ||
PRIORITY: str = "priority" | ||
STATUS: str = "status" | ||
|
||
|
||
class SpecialFields(ConstantVariable): | ||
# Event with unique payload | ||
NAME: str = "name" | ||
ASSIGNEE: str = "assignee_add" | ||
COMMENT: str = "comment" | ||
DUE_DATE: str = "due_date" | ||
MOVED: str = "section_moved" | ||
TIME_ESTIMATE: str = "time_estimate" | ||
TIME_SPENT: str = "time_spent" | ||
|
||
|
||
class SpammyFields(ConstantVariable): | ||
TAG: str = "tag" | ||
TAG_REMOVED: str = "tag_removed" | ||
UNASSIGN: str = "assignee_rem" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import re | ||
from typing import Any, Dict, Optional, Union | ||
from urllib.parse import urljoin | ||
|
||
import requests | ||
from django.utils.translation import gettext as _ | ||
from typing_extensions import override | ||
|
||
from zerver.lib.exceptions import ErrorCode, WebhookError | ||
from zerver.lib.outgoing_http import OutgoingSession | ||
from zerver.webhooks.clickup import EventItemType | ||
|
||
|
||
class APIUnavailableCallBackError(WebhookError): | ||
"""Intended as an exception for when an integration | ||
couldn't reach external API server when calling back | ||
from Zulip app. | ||
Exception when callback request has timed out or received | ||
connection error. | ||
""" | ||
|
||
code = ErrorCode.REQUEST_TIMEOUT | ||
http_status_code = 200 | ||
data_fields = ["webhook_name"] | ||
|
||
def __init__(self) -> None: | ||
super().__init__() | ||
|
||
@staticmethod | ||
@override | ||
def msg_format() -> str: | ||
return _("{webhook_name} integration couldn't reach an external API service; ignoring") | ||
|
||
|
||
class BadRequestCallBackError(WebhookError): | ||
"""Intended as an exception for when an integration | ||
makes a bad request to external API server. | ||
Exception when callback request has an invalid format. | ||
""" | ||
|
||
code = ErrorCode.BAD_REQUEST | ||
http_status_code = 200 | ||
data_fields = ["webhook_name", "error_detail"] | ||
|
||
def __init__(self, error_detail: Optional[Union[str, int]]) -> None: | ||
super().__init__() | ||
self.error_detail = error_detail | ||
|
||
@staticmethod | ||
@override | ||
def msg_format() -> str: | ||
return _( | ||
"{webhook_name} integration tries to make a bad outgoing request: {error_detail}; ignoring" | ||
) | ||
|
||
|
||
class ClickUpSession(OutgoingSession): | ||
def __init__(self, **kwargs: Any) -> None: | ||
super().__init__(role="clickup", timeout=5, **kwargs) # nocoverage | ||
|
||
|
||
def verify_url_path(path: str) -> bool: | ||
parts = path.split("/") | ||
if len(parts) < 2 or parts[0] not in EventItemType.as_list() or parts[1] == "": | ||
return False | ||
pattern = r"^[a-zA-Z0-9_-]+$" | ||
match = re.match(pattern, parts[1]) | ||
return match is not None and match.group() == parts[1] | ||
|
||
|
||
def make_clickup_request(path: str, api_key: str) -> Dict[str, Any]: | ||
if verify_url_path(path) is False: | ||
raise BadRequestCallBackError("Invalid path") | ||
headers: Dict[str, str] = { | ||
"Content-Type": "application/json", | ||
"Authorization": api_key, | ||
} | ||
|
||
try: | ||
base_url = "https://api.clickup.com/api/v2/" | ||
api_endpoint = urljoin(base_url, path) | ||
response = ClickUpSession(headers=headers).get( | ||
api_endpoint, | ||
) | ||
response.raise_for_status() | ||
except (requests.ConnectionError, requests.Timeout): | ||
raise APIUnavailableCallBackError | ||
except requests.HTTPError as e: | ||
raise BadRequestCallBackError(e.response.status_code) | ||
|
||
return response.json() | ||
|
||
|
||
def get_list(api_key: str, list_id: str) -> Dict[str, Any]: | ||
data = make_clickup_request(f"list/{list_id}", api_key) | ||
return data | ||
|
||
|
||
def get_task(api_key: str, task_id: str) -> Dict[str, Any]: | ||
data = make_clickup_request(f"task/{task_id}", api_key) | ||
return data | ||
|
||
|
||
def get_folder(api_key: str, folder_id: str) -> Dict[str, Any]: | ||
data = make_clickup_request(f"folder/{folder_id}", api_key) | ||
return data | ||
|
||
|
||
def get_goal(api_key: str, goal_id: str) -> Dict[str, Any]: | ||
data = make_clickup_request(f"goal/{goal_id}", api_key) | ||
return data | ||
|
||
|
||
def get_space(api_key: str, space_id: str) -> Dict[str, Any]: | ||
data = make_clickup_request(f"space/{space_id}", api_key) | ||
return data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"id": "457", | ||
"name": "Lord Foldemort", | ||
"orderindex": 0, | ||
"override_statuses": false, | ||
"hidden": false, | ||
"space": { | ||
"id": "789", | ||
"name": "Space Name", | ||
"access": true | ||
}, | ||
"task_count": "0", | ||
"lists": [] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"goal": { | ||
"id": "e53a033c-900e-462d-a849-4a216b06d930", | ||
"name": "hat-trick", | ||
"team_id": "512", | ||
"date_created": "1568044355026", | ||
"start_date": null, | ||
"due_date": "1568036964079", | ||
"description": "Updated Goal Description", | ||
"private": false, | ||
"archived": false, | ||
"creator": 183, | ||
"color": "#32a852", | ||
"pretty_id": "6", | ||
"multiple_owners": true, | ||
"folder_id": null, | ||
"members": [], | ||
"owners": [ | ||
{ | ||
"id": 182, | ||
"username": "Pieter CK", | ||
"email": "[email protected]", | ||
"color": "#7b68ee", | ||
"initials": "PK", | ||
"profilePicture": "https://attachments-public.clickup.com/profilePictures/182_abc.jpg" | ||
} | ||
], | ||
"key_results": [], | ||
"percent_completed": 0, | ||
"history": [], | ||
"pretty_url": "https://app.clickup.com/512/goals/6" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
{ | ||
"id": "124", | ||
"name": "List-an al Gaib", | ||
"orderindex": 1, | ||
"content": "Updated List Content", | ||
"status": { | ||
"status": "red", | ||
"color": "#e50000", | ||
"hide_label": true | ||
}, | ||
"priority": { | ||
"priority": "high", | ||
"color": "#f50000" | ||
}, | ||
"assignee": null, | ||
"due_date": "1567780450202", | ||
"due_date_time": true, | ||
"start_date": null, | ||
"start_date_time": null, | ||
"folder": { | ||
"id": "456", | ||
"name": "Folder Name", | ||
"hidden": false, | ||
"access": true | ||
}, | ||
"space": { | ||
"id": "789", | ||
"name": "Space Name", | ||
"access": true | ||
}, | ||
"inbound_address": "add.task.124.ac725f.31518a6a-05bb-4997-92a6-1dcfe2f527ca@tasks.clickup.com", | ||
"archived": false, | ||
"override_statuses": false, | ||
"statuses": [ | ||
{ | ||
"status": "to do", | ||
"orderindex": 0, | ||
"color": "#d3d3d3", | ||
"type": "open" | ||
}, | ||
{ | ||
"status": "complete", | ||
"orderindex": 1, | ||
"color": "#6bc950", | ||
"type": "closed" | ||
} | ||
], | ||
"permission_level": "create" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
{ | ||
"id": "790", | ||
"name": "the Milky Way", | ||
"private": false, | ||
"statuses": [ | ||
{ | ||
"status": "to do", | ||
"type": "open", | ||
"orderindex": 0, | ||
"color": "#d3d3d3" | ||
}, | ||
{ | ||
"status": "complete", | ||
"type": "closed", | ||
"orderindex": 1, | ||
"color": "#6bc950" | ||
} | ||
], | ||
"multiple_assignees": false, | ||
"features": { | ||
"due_dates": { | ||
"enabled": false, | ||
"start_date": false, | ||
"remap_due_dates": false, | ||
"remap_closed_due_date": false | ||
}, | ||
"time_tracking": { | ||
"enabled": false | ||
}, | ||
"tags": { | ||
"enabled": false | ||
}, | ||
"time_estimates": { | ||
"enabled": false | ||
}, | ||
"checklists": { | ||
"enabled": true | ||
}, | ||
"custom_fields": { | ||
"enabled": true | ||
}, | ||
"remap_dependencies": { | ||
"enabled": false | ||
}, | ||
"dependency_warning": { | ||
"enabled": false | ||
}, | ||
"portfolios": { | ||
"enabled": false | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
{ | ||
"id": "string", | ||
"custom_id": "string", | ||
"custom_item_id": 0, | ||
"name": "Tanswer", | ||
"text_content": "string", | ||
"description": "string", | ||
"status": { | ||
"status": "in progress", | ||
"color": "#d3d3d3", | ||
"orderindex": 1, | ||
"type": "custom" | ||
}, | ||
"orderindex": "string", | ||
"date_created": "string", | ||
"date_updated": "string", | ||
"date_closed": "string", | ||
"creator": { | ||
"id": 183, | ||
"username": "Pieter CK", | ||
"color": "#827718", | ||
"profilePicture": "https://attachments-public.clickup.com/profilePictures/183_abc.jpg" | ||
}, | ||
"assignees": ["string"], | ||
"checklists": ["string"], | ||
"tags": ["string"], | ||
"parent": "string", | ||
"priority": "string", | ||
"due_date": "string", | ||
"start_date": "string", | ||
"time_estimate": "string", | ||
"time_spent": "string", | ||
"custom_fields": [ | ||
{ | ||
"id": "string", | ||
"name": "string", | ||
"type": "string", | ||
"type_config": {}, | ||
"date_created": "string", | ||
"hide_from_guests": true, | ||
"value": { | ||
"id": 183, | ||
"username": "Pieter CK", | ||
"email": "[email protected]", | ||
"color": "#7b68ee", | ||
"initials": "PK", | ||
"profilePicture": null | ||
}, | ||
"required": true | ||
} | ||
], | ||
"list": { | ||
"id": "123" | ||
}, | ||
"folder": { | ||
"id": "456" | ||
}, | ||
"space": { | ||
"id": "789" | ||
}, | ||
"url": "https://app.clickup.com/XXXXXXXX/home", | ||
"markdown_description": "string" | ||
} |
Oops, something went wrong.