diff --git a/static/images/integrations/bot_avatars/clickup.png b/static/images/integrations/bot_avatars/clickup.png new file mode 100644 index 00000000000000..39197b44d32309 Binary files /dev/null and b/static/images/integrations/bot_avatars/clickup.png differ diff --git a/static/images/integrations/clickup/001.png b/static/images/integrations/clickup/001.png new file mode 100644 index 00000000000000..c2f51c0c0982e5 Binary files /dev/null and b/static/images/integrations/clickup/001.png differ diff --git a/static/images/integrations/logos/clickup.svg b/static/images/integrations/logos/clickup.svg new file mode 100644 index 00000000000000..18f875d5cc8500 --- /dev/null +++ b/static/images/integrations/logos/clickup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py index aa0c61d7597aed..3514fc794c049a 100644 --- a/zerver/lib/integrations.py +++ b/zerver/lib/integrations.py @@ -379,6 +379,7 @@ def __init__(self, name: str, *args: Any, **kwargs: Any) -> None: WebhookIntegration("buildbot", ["continuous-integration"], display_name="Buildbot"), WebhookIntegration("canarytoken", ["monitoring"], display_name="Thinkst Canarytokens"), WebhookIntegration("circleci", ["continuous-integration"], display_name="CircleCI"), + WebhookIntegration("clickup", ["project-management"], display_name="ClickUp"), WebhookIntegration("clubhouse", ["project-management"]), WebhookIntegration("codeship", ["continuous-integration", "deployment"]), WebhookIntegration("crashlytics", ["monitoring"]), @@ -730,6 +731,7 @@ def __init__(self, name: str, *args: Any, **kwargs: Any) -> None: ScreenshotConfig("bitbucket_job_completed.json", image_name="001.png"), ScreenshotConfig("github_job_completed.json", image_name="002.png"), ], + "clickup": [ScreenshotConfig("task_moved.json")], "clubhouse": [ScreenshotConfig("story_create.json")], "codeship": [ScreenshotConfig("error_build.json")], "crashlytics": [ScreenshotConfig("issue_message.json")], diff --git a/zerver/webhooks/clickup/__init__.py b/zerver/webhooks/clickup/__init__.py new file mode 100644 index 00000000000000..8b137891791fe9 --- /dev/null +++ b/zerver/webhooks/clickup/__init__.py @@ -0,0 +1 @@ + diff --git a/zerver/webhooks/clickup/callback_fixtures/get_folder.json b/zerver/webhooks/clickup/callback_fixtures/get_folder.json new file mode 100644 index 00000000000000..f646c9fdac0520 --- /dev/null +++ b/zerver/webhooks/clickup/callback_fixtures/get_folder.json @@ -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": [] +} diff --git a/zerver/webhooks/clickup/callback_fixtures/get_goal.json b/zerver/webhooks/clickup/callback_fixtures/get_goal.json new file mode 100644 index 00000000000000..733317c1e2be02 --- /dev/null +++ b/zerver/webhooks/clickup/callback_fixtures/get_goal.json @@ -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": "kwok.pieter@gmail.com", + "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" + } +} diff --git a/zerver/webhooks/clickup/callback_fixtures/get_list.json b/zerver/webhooks/clickup/callback_fixtures/get_list.json new file mode 100644 index 00000000000000..1fa3309a5f295b --- /dev/null +++ b/zerver/webhooks/clickup/callback_fixtures/get_list.json @@ -0,0 +1,49 @@ +{ + "id": "124", + "name": "Listener", + "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" +} diff --git a/zerver/webhooks/clickup/callback_fixtures/get_space.json b/zerver/webhooks/clickup/callback_fixtures/get_space.json new file mode 100644 index 00000000000000..d19af504b23fb4 --- /dev/null +++ b/zerver/webhooks/clickup/callback_fixtures/get_space.json @@ -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 + } + } +} diff --git a/zerver/webhooks/clickup/callback_fixtures/get_task.json b/zerver/webhooks/clickup/callback_fixtures/get_task.json new file mode 100644 index 00000000000000..146db98e6ad868 --- /dev/null +++ b/zerver/webhooks/clickup/callback_fixtures/get_task.json @@ -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": "kwok.pieter@gmail.com", + "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" +} diff --git a/zerver/webhooks/clickup/doc.md b/zerver/webhooks/clickup/doc.md new file mode 100644 index 00000000000000..01d9606b9c383c --- /dev/null +++ b/zerver/webhooks/clickup/doc.md @@ -0,0 +1,88 @@ +# Zulip ClickUp integration + +Get Zulip notifications for your ClickUp space! + +!!! tip "" + + [Zapier](./zapier) is usually a simpler way to integrate ClickUp + with Zulip. + +{start_tabs} + +1. {!create-channel.md!} + +1. {!create-an-incoming-webhook.md!} + +1. {!generate-webhook-url-basic.md!} + +1. Collect your ClickUp **Team ID** by going to your ClickUp home view. + The URL should look like `https://app.clickup.com//home`. + Note down the ``. + +1. Collect your ClickUp **Client ID** and **Client Secret** by following these steps: + + - Go to your [ClickUp API menu][1] and click **Create an App**. + + - You will be prompted for **Redirect URL(s)**, enter the URL for your Zulip organization. + e.g., `{{ zulip_url }}`. + + - Note down the **Client ID** and **Client Secret** + +1. You're now going to need to run a ClickUp configuration script from a + computer (any computer) connected to the internet. It won't make any + changes to the computer. + + Make sure you have a working copy of Python. If you're running + macOS or Linux, you very likely already do. If you're running + Windows you may or may not. If you don't have Python, follow the + installation instructions [here][2]. + + !!! tip "" + + You do not need the latest version of Python; anything 2.7 or + higher will do. + +1. Download [zulip-clickup.py][3]. + + !!! tip "" + + Ctrl + s or Cmd + s + on that page should work in most browsers. + +1. Run the `zulip-clickup.py` script in a terminal, after replacing the all caps + arguments with the values collected above. + + ``` + python zulip-clickup.py --clickup-team-id CLICKUP_TEAM_ID \ + --clickup-client-id CLICKUP_CLIENT_ID \ + --clickup-client-secret CLICKUP_CLIENT_SECRET \ + --zulip-webhook-url "ZULIP_WEBHOOK_URL" + ``` + + !!! warn "" + + **Note**: Make sure that you wrap the webhook URL generated above + in quotes when supplying it on the command-line, as shown above. + +1. Follow the instructions in the terminal and keep an eye on your browser as you + will be redirected to a ClickUp authorization page. + +{end_tabs} + +{!congrats.md!} + +![](/static/images/integrations/clickup/001.png) + +### Related documentation + +- [Zapier ClickUp integration][4] + +{!webhooks-url-specification.md!} + +[1]: https://app.clickup.com/settings/team/clickup-api + +[2]: https://realpython.com/installing-python/ + +[3]: https://raw.githubusercontent.com/zulip/python-zulip-api/main/zulip/integrations/clickup/zulip_clickup.py + +[4]: https://zapier.com/apps/clickup/integrations#zap-template-list diff --git a/zerver/webhooks/clickup/fixtures/folder_created.json b/zerver/webhooks/clickup/fixtures/folder_created.json new file mode 100644 index 00000000000000..69ca7103079cc7 --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/folder_created.json @@ -0,0 +1,5 @@ +{ + "event": "folderCreated", + "folder_id": "96772212", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} diff --git a/zerver/webhooks/clickup/fixtures/folder_deleted.json b/zerver/webhooks/clickup/fixtures/folder_deleted.json new file mode 100644 index 00000000000000..19671f01194d3c --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/folder_deleted.json @@ -0,0 +1,5 @@ +{ + "event": "folderDeleted", + "folder_id": "96772212", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} diff --git a/zerver/webhooks/clickup/fixtures/folder_updated.json b/zerver/webhooks/clickup/fixtures/folder_updated.json new file mode 100644 index 00000000000000..d1b697320b4cfc --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/folder_updated.json @@ -0,0 +1,5 @@ +{ + "event": "folderUpdated", + "folder_id": "96772212", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} diff --git a/zerver/webhooks/clickup/fixtures/goal_created.json b/zerver/webhooks/clickup/fixtures/goal_created.json new file mode 100644 index 00000000000000..7f8e5ce8d4a3f7 --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/goal_created.json @@ -0,0 +1,5 @@ +{ + "event": "goalCreated", + "goal_id": "a23e5a3d-74b5-44c2-ab53-917ebe85045a", + "webhook_id": "d5eddb2d-db2b-49e9-87d4-bc6cfbe2313b" +} diff --git a/zerver/webhooks/clickup/fixtures/goal_deleted.json b/zerver/webhooks/clickup/fixtures/goal_deleted.json new file mode 100644 index 00000000000000..626f0e7bd739e0 --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/goal_deleted.json @@ -0,0 +1,5 @@ +{ + "event": "goalDeleted", + "goal_id": "a23e5a3d-74b5-44c2-ab53-917ebe85045a", + "webhook_id": "d5eddb2d-db2b-49e9-87d4-bc6cfbe2313b" +} diff --git a/zerver/webhooks/clickup/fixtures/goal_updated.json b/zerver/webhooks/clickup/fixtures/goal_updated.json new file mode 100644 index 00000000000000..97888fe9cba496 --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/goal_updated.json @@ -0,0 +1,5 @@ +{ + "event": "goalUpdated", + "goal_id": "a23e5a3d-74b5-44c2-ab53-917ebe85045a", + "webhook_id": "d5eddb2d-db2b-49e9-87d4-bc6cfbe2313b" +} diff --git a/zerver/webhooks/clickup/fixtures/list_created.json b/zerver/webhooks/clickup/fixtures/list_created.json new file mode 100644 index 00000000000000..290b670327574d --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/list_created.json @@ -0,0 +1,5 @@ +{ + "event": "listCreated", + "list_id": "901601848935", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} diff --git a/zerver/webhooks/clickup/fixtures/list_deleted.json b/zerver/webhooks/clickup/fixtures/list_deleted.json new file mode 100644 index 00000000000000..6f29a35e66265b --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/list_deleted.json @@ -0,0 +1,5 @@ +{ + "event": "listDeleted", + "list_id": "901601848935", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} diff --git a/zerver/webhooks/clickup/fixtures/list_updated.json b/zerver/webhooks/clickup/fixtures/list_updated.json new file mode 100644 index 00000000000000..6abe0b0566b012 --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/list_updated.json @@ -0,0 +1,26 @@ +{ + "event": "listUpdated", + "history_items": [ + { + "id": "8a2f82db-7718-4fdb-9493-4849e67f009d", + "type": 6, + "date": "1642740510345", + "field": "name", + "parent_id": "162641285", + "data": {}, + "source": null, + "user": { + "id": 183, + "username": "Pieter", + "email": "kwok.pieter@gmail.com", + "color": "#7b68ee", + "initials": "P", + "profilePicture": null + }, + "before": "webhook payloads 2", + "after": "Webhook payloads round 2" + } + ], + "list_id": "901601848935", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" + } diff --git a/zerver/webhooks/clickup/fixtures/payload_with_spammy_field.json b/zerver/webhooks/clickup/fixtures/payload_with_spammy_field.json new file mode 100644 index 00000000000000..e4bbdee602d67d --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/payload_with_spammy_field.json @@ -0,0 +1,33 @@ +{ + "event": "taskUpdated", + "history_items": [ + { + "id": "2800797048554170804", + "type": 1, + "date": "1642736652800", + "field": "tag", + "parent_id": "162641062", + "data": {}, + "source": null, + "user": { + "id": 183, + "username": "John", + "email": "john@company.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": null, + "after": [ + { + "name": "def", + "tag_fg": "#FF4081", + "tag_bg": "#FF4081", + "creator": 2770032 + } + ] + } + ], + "task_id": "86cvyxabb", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" + } diff --git a/zerver/webhooks/clickup/fixtures/space_created.json b/zerver/webhooks/clickup/fixtures/space_created.json new file mode 100644 index 00000000000000..331c82832b1a7f --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/space_created.json @@ -0,0 +1,5 @@ +{ + "event": "spaceCreated", + "space_id": "90160869743", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} diff --git a/zerver/webhooks/clickup/fixtures/space_deleted.json b/zerver/webhooks/clickup/fixtures/space_deleted.json new file mode 100644 index 00000000000000..c5d95f29a0b945 --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/space_deleted.json @@ -0,0 +1,5 @@ +{ + "event": "spaceDeleted", + "space_id": "90160869743", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} diff --git a/zerver/webhooks/clickup/fixtures/space_updated.json b/zerver/webhooks/clickup/fixtures/space_updated.json new file mode 100644 index 00000000000000..53d9e36468a77b --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/space_updated.json @@ -0,0 +1,5 @@ +{ + "event": "spaceUpdated", + "space_id": "90160869743", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} diff --git a/zerver/webhooks/clickup/fixtures/task_created.json b/zerver/webhooks/clickup/fixtures/task_created.json new file mode 100644 index 00000000000000..b2ad1abfa2d540 --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/task_created.json @@ -0,0 +1,57 @@ +{ + "event": "taskCreated", + "history_items": [ + { + "id": "2800763136717140857", + "type": 1, + "date": "1642734631523", + "field": "status", + "parent_id": "162641062", + "data": { + "status_type": "open" + }, + "source": null, + "user": { + "id": 183, + "username": "Pieter", + "email": "pieterceka123@gmail.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": { + "status": null, + "color": "#000000", + "type": "removed", + "orderindex": -1 + }, + "after": { + "status": "to do", + "color": "#f9d900", + "orderindex": 0, + "type": "open" + } + }, + { + "id": "2800763136700363640", + "type": 1, + "date": "1642734631523", + "field": "task_creation", + "parent_id": "162641062", + "data": {}, + "source": null, + "user": { + "id": 183, + "username": "Pieter", + "email": "kwok.pieter@gmail.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": null, + "after": null + } + ], + "task_id": "86cvyxabb", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} diff --git a/zerver/webhooks/clickup/fixtures/task_deleted.json b/zerver/webhooks/clickup/fixtures/task_deleted.json new file mode 100644 index 00000000000000..540458df826bf0 --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/task_deleted.json @@ -0,0 +1,5 @@ +{ + "event": "taskDeleted", + "task_id": "86cvyxabb", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} diff --git a/zerver/webhooks/clickup/fixtures/task_moved.json b/zerver/webhooks/clickup/fixtures/task_moved.json new file mode 100644 index 00000000000000..46f66c88541eb1 --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/task_moved.json @@ -0,0 +1,52 @@ +{ + "event": "taskUpdated", + "history_items": [ + { + "id": "2800800851630274181", + "type": 1, + "date": "1642736879339", + "field": "section_moved", + "parent_id": "162641285", + "data": { + "mute_notifications": true + }, + "source": null, + "user": { + "id": 183, + "username": "Pieter", + "email": "kwok.pieter@gmail.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": { + "id": "162641062", + "name": "Webhook payloads", + "category": { + "id": "96771950", + "name": "hidden", + "hidden": true + }, + "project": { + "id": "7002367", + "name": "This is my API Space" + } + }, + "after": { + "id": "162641285", + "name": "webhook payloads 2", + "category": { + "id": "96772049", + "name": "hidden", + "hidden": true + }, + "project": { + "id": "7002367", + "name": "This is my API Space" + } + } + } + ], + "task_id": "86cvyxabb", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" + } diff --git a/zerver/webhooks/clickup/fixtures/task_updated_assignee.json b/zerver/webhooks/clickup/fixtures/task_updated_assignee.json new file mode 100644 index 00000000000000..b16d118ecd3f3c --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/task_updated_assignee.json @@ -0,0 +1,32 @@ +{ + "event": "taskUpdated", + "history_items": [ + { + "id": "2800789353868594308", + "type": 1, + "date": "1642736194135", + "field": "assignee_add", + "parent_id": "162641062", + "data": {}, + "source": null, + "user": { + "id": 183, + "username": "Pieter", + "email": "kwok.pieter@gmail.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "after": { + "id": 184, + "username": "Sam", + "email": "sam@company.com", + "color": "#7b68ee", + "initials": "S", + "profilePicture": null + } + } + ], + "task_id": "86cvyxabb", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} diff --git a/zerver/webhooks/clickup/fixtures/task_updated_comment.json b/zerver/webhooks/clickup/fixtures/task_updated_comment.json new file mode 100644 index 00000000000000..d1dd41018e7d9b --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/task_updated_comment.json @@ -0,0 +1,85 @@ +{ + "event": "taskUpdated", + "history_items": [ + { + "id": "2800803631413624919", + "type": 1, + "date": "1642737045116", + "field": "comment", + "parent_id": "162641285", + "data": {}, + "source": null, + "user": { + "id": 183, + "username": "Pieter", + "email": "pieterceka123@gmail.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": null, + "after": "648893191", + "comment": { + "id": "648893191", + "date": "1642737045116", + "parent": "1vj38vv", + "type": 1, + "comment": [ + { + "text": "comment abc1234 56789", + "attributes": {} + }, + { + "text": "\n", + "attributes": { + "block-id": "block-4c8fe54f-7bff-4b7b-92a2-9142068983ea" + } + } + ], + "text_content": "comment abc1234 56789\n", + "x": null, + "y": null, + "image_y": null, + "image_x": null, + "page": null, + "comment_number": null, + "page_id": null, + "page_name": null, + "view_id": null, + "view_name": null, + "team": null, + "user": { + "id": 183, + "username": "Pieter", + "email": "pieterceka123@gmail.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "new_thread_count": 0, + "new_mentioned_thread_count": 0, + "email_attachments": [], + "threaded_users": [], + "threaded_replies": 0, + "threaded_assignees": 0, + "threaded_assignees_members": [], + "threaded_unresolved_count": 0, + "thread_followers": [ + { + "id": 183, + "username": "Pieter", + "email": "pieterceka123@gmail.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + } + ], + "group_thread_followers": [], + "reactions": [], + "emails": [] + } + } + ], + "task_id": "86cvyxabb", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" + } diff --git a/zerver/webhooks/clickup/fixtures/task_updated_due_date.json b/zerver/webhooks/clickup/fixtures/task_updated_due_date.json new file mode 100644 index 00000000000000..45610c16c0f10a --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/task_updated_due_date.json @@ -0,0 +1,29 @@ +{ + "event": "taskUpdated", + "history_items": [ + { + "id": "2800792714143635886", + "type": 1, + "date": "1642736394447", + "field": "due_date", + "parent_id": "162641062", + "data": { + "due_date_time": true, + "old_due_date_time": false + }, + "source": null, + "user": { + "id": 183, + "username": "Pieter", + "email": "kwok.pieter@gmail.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": "1642701600000", + "after": "1643608800000" + } + ], + "task_id": "86cvyxabb", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" + } diff --git a/zerver/webhooks/clickup/fixtures/task_updated_priority.json b/zerver/webhooks/clickup/fixtures/task_updated_priority.json new file mode 100644 index 00000000000000..c5825a6b435807 --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/task_updated_priority.json @@ -0,0 +1,31 @@ +{ + "event": "taskUpdated", + "history_items": [ + { + "id": "2800773800802162647", + "type": 1, + "date": "1642735267148", + "field": "priority", + "parent_id": "162641062", + "data": {}, + "source": null, + "user": { + "id": 183, + "username": "Pieter", + "email": "kwok.pieter@gmail.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": null, + "after": { + "id": "2", + "priority": "high", + "color": "#ffcc00", + "orderindex": "2" + } + } + ], + "task_id": "86cvyxabb", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" + } diff --git a/zerver/webhooks/clickup/fixtures/task_updated_status.json b/zerver/webhooks/clickup/fixtures/task_updated_status.json new file mode 100644 index 00000000000000..395ff54cb1a6ab --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/task_updated_status.json @@ -0,0 +1,38 @@ +{ + "event": "taskUpdated", + "history_items": [ + { + "id": "2800787326392370170", + "type": 1, + "date": "1642736073330", + "field": "status", + "parent_id": "162641062", + "data": { + "status_type": "custom" + }, + "source": null, + "user": { + "id": 183, + "username": "Pieter", + "email": "kwok.pieter@gmail.com", + "color": "#7b68ee", + "initials": "J", + "profilePicture": null + }, + "before": { + "status": "to do", + "color": "#f9d900", + "orderindex": 0, + "type": "open" + }, + "after": { + "status": "in progress", + "color": "#7C4DFF", + "orderindex": 1, + "type": "custom" + } + } + ], + "task_id": "86cvyxabb", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" +} diff --git a/zerver/webhooks/clickup/fixtures/task_updated_time_estimate.json b/zerver/webhooks/clickup/fixtures/task_updated_time_estimate.json new file mode 100644 index 00000000000000..09862ebbb19d86 --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/task_updated_time_estimate.json @@ -0,0 +1,38 @@ +{ + "event": "taskUpdated", + "history_items": [ + { + "id": "2800808904123520175", + "type": 1, + "date": "1642737359443", + "field": "time_estimate", + "parent_id": "162641285", + "data": { + "time_estimate_string": "1 hour 30 minutes", + "old_time_estimate_string": null, + "rolled_up_time_estimate": 5400000, + "time_estimate": 5400000, + "time_estimates_by_user": [ + { + "userid": 2770032, + "user_time_estimate": "5400000", + "user_rollup_time_estimate": "5400000" + } + ] + }, + "source": null, + "user": { + "id": 183, + "username": "Pieter", + "email": "kwok.pieter@gmail.com", + "color": "#7b68ee", + "initials": "P", + "profilePicture": null + }, + "before": null, + "after": "5400000" + } + ], + "task_id": "86cvyxabb", + "webhook_id": "7fa3ec74-69a8-4530-a251-8a13730bd204" + } diff --git a/zerver/webhooks/clickup/fixtures/task_updated_time_spent.json b/zerver/webhooks/clickup/fixtures/task_updated_time_spent.json new file mode 100644 index 00000000000000..1b44d6d3c6aea2 --- /dev/null +++ b/zerver/webhooks/clickup/fixtures/task_updated_time_spent.json @@ -0,0 +1,37 @@ +{ + "event": "taskUpdated", + "history_items": [ + { + "id": "3945907824924417727", + "type": "1", + "date": "1710990573849", + "field": "time_spent", + "parent_id": "163597292", + "data": {"total_time": "68520000", "rollup_time": "68520000"}, + "source": null, + "user": { + "id": "37621629", + "username": "Pieter", + "email": "pieterceka123@gmail.com", + "color": "#5f7c8a", + "initials": "P", + "profilePicture": null + }, + "before": null, + "after": { + "id": "3945907824924425939", + "start": "1710972573656", + "end": "1710990573656", + "time": "18000000", + "source": "clickup", + "date_added": "1710990573849" + } + } + ], + "task_id": "86cvyxabb", + "data": { + "description": "Time Tracking Created", + "interval_id": "3945907824924425939" + }, + "webhook_id": "4c21a84b-d0d8-41f7-978e-4fea0776f150" +} diff --git a/zerver/webhooks/clickup/tests.py b/zerver/webhooks/clickup/tests.py new file mode 100644 index 00000000000000..8d96b558b29c99 --- /dev/null +++ b/zerver/webhooks/clickup/tests.py @@ -0,0 +1,323 @@ +import json +from typing import Any +from unittest.mock import MagicMock, patch + +from typing_extensions import override + +from zerver.lib.test_classes import WebhookTestCase + +from .api_endpoints import get_clickup_api_data + +EXPECTED_TOPIC = "ClickUp Notification" + + +class ClickUpHookTests(WebhookTestCase): + CHANNEL_NAME = "ClickUp" + URL_TEMPLATE = "/api/v1/external/clickup?api_key={api_key}&stream={stream}&team_id=XXXXXXX&clickup_api_key=123" + FIXTURE_DIR_NAME = "clickup" + WEBHOOK_DIR_NAME = "clickup" + + @override + def setUp(self) -> None: + super().setUp() + self.mock_get_clickup_api_data = patch( + "zerver.webhooks.clickup.view.get_clickup_api_data" + ).start() + self.mock_get_clickup_api_data.side_effect = self.mocked_get_clickup_api_data + + @override + def tearDown(self) -> None: + self.mock_get_clickup_api_data.stop() + super().tearDown() + + def mocked_get_clickup_api_data(self, clickup_api_path: str, **kwargs: Any) -> None: + item = clickup_api_path.split("/")[0] + with open(f"zerver/webhooks/clickup/callback_fixtures/get_{item}.json") as f: + return json.load(f) + + def test_task_created(self) -> None: + expected_message = ( + ":new: **[Task: Tanswer](https://app.clickup.com/XXXXXXX/home)** has been created in your ClickUp space!" + "\n - Created by: **Pieter CK**" + ) + + self.check_webhook( + fixture_name="task_created", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_task_deleted(self) -> None: + self.url = self.build_webhook_url(team_id="XXXXXXXX", clickup_api_key="123") + expected_message = ":trash_can: A Task has been deleted from your ClickUp space!" + + self.check_webhook( + fixture_name="task_deleted", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_updated_time_spent(self) -> None: + expected_message = ( + "**[Task: Tanswer](https://app.clickup.com/XXXXXXX/home)** has been updated!\n" + "~~~ quote\n" + " :stopwatch: Time spent changed to **19:02:00**\n" + "~~~" + ) + + self.check_webhook( + fixture_name="task_updated_time_spent", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_task_updated_time_estimate(self) -> None: + expected_message = ( + "**[Task: Tanswer](https://app.clickup.com/XXXXXXX/home)** has been updated!\n" + "~~~ quote\n" + " :ruler: Time estimate changed from **None** to **1 hour 30 minutes** by **Pieter**\n" + "~~~" + ) + + self.check_webhook( + fixture_name="task_updated_time_estimate", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_task_updated_comment(self) -> None: + expected_message = ( + "**[Task: Tanswer](https://app.clickup.com/XXXXXXX/home)** has been updated!\n" + "~~~ quote\n" + " :speaking_head: Commented by **Pieter**\n" + "~~~" + ) + + self.check_webhook( + fixture_name="task_updated_comment", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_task_moved(self) -> None: + expected_message = ( + "**[Task: Tanswer](https://app.clickup.com/XXXXXXX/home)** has been updated!\n" + "~~~ quote\n" + " :folder: Moved from **Webhook payloads** to **webhook payloads 2**\n" + "~~~" + ) + + self.check_webhook( + fixture_name="task_moved", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_task_updated_assignee(self) -> None: + expected_message = ( + "**[Task: Tanswer](https://app.clickup.com/XXXXXXX/home)** has been updated!\n" + "~~~ quote\n" + " :silhouette: Now assigned to **Sam**\n" + "~~~" + ) + + self.check_webhook( + fixture_name="task_updated_assignee", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_task_updated_due_date(self) -> None: + expected_message = ( + "**[Task: Tanswer](https://app.clickup.com/XXXXXXX/home)** has been updated!\n" + "~~~ quote\n" + " :spiral_calendar: Due date updated from to \n" + "~~~" + ) + + self.check_webhook( + fixture_name="task_updated_due_date", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_task_updated_priority(self) -> None: + expected_message = ( + "**[Task: Tanswer](https://app.clickup.com/XXXXXXX/home)** has been updated!\n" + "~~~ quote\n" + " :note: Updated task priority from **None** to **high**\n" + "~~~" + ) + + self.check_webhook( + fixture_name="task_updated_priority", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_task_updated_status(self) -> None: + expected_message = ( + "**[Task: Tanswer](https://app.clickup.com/XXXXXXX/home)** has been updated!\n" + "~~~ quote\n" + " :note: Updated task status from **to do** to **in progress**\n" + "~~~" + ) + + self.check_webhook( + fixture_name="task_updated_status", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_list_created(self) -> None: + expected_message = ":new: **[List: Listener](https://app.clickup.com/XXXXXXX/home)** has been created in your ClickUp space!" + self.check_webhook( + fixture_name="list_created", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_list_deleted(self) -> None: + expected_message = ":trash_can: A List has been deleted from your ClickUp space!" + self.check_webhook( + fixture_name="list_deleted", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_list_updated(self) -> None: + expected_message = ( + "**[List: Listener](https://app.clickup.com/XXXXXXX/home)** has been updated!\n" + "~~~ quote\n" + " :pencil: Renamed from **webhook payloads 2** to **Webhook payloads round 2**\n" + "~~~" + ) + self.check_webhook( + fixture_name="list_updated", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_folder_created(self) -> None: + expected_message = ":new: **[Folder: Lord Foldemort](https://app.clickup.com/XXXXXXX/home)** has been created in your ClickUp space!" + self.check_webhook( + fixture_name="folder_created", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_folder_deleted(self) -> None: + expected_message = ":trash_can: A Folder has been deleted from your ClickUp space!" + self.check_webhook( + fixture_name="folder_deleted", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_space_created(self) -> None: + expected_message = ":new: **[Space: the Milky Way](https://app.clickup.com/XXXXXXX/home)** has been created in your ClickUp space!" + self.check_webhook( + fixture_name="space_created", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_space_deleted(self) -> None: + expected_message = ":trash_can: A Space has been deleted from your ClickUp space!" + self.check_webhook( + fixture_name="space_deleted", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_space_updated(self) -> None: + expected_message = ( + "**[Space: the Milky Way](https://app.clickup.com/XXXXXXX/home)** has been updated!" + ) + self.check_webhook( + fixture_name="space_updated", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_goal_created(self) -> None: + expected_message = ":new: **[Goal: hat-trick](https://app.clickup.com/512/goals/6)** has been created in your ClickUp space!" + self.check_webhook( + fixture_name="goal_created", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_goal_updated(self) -> None: + expected_message = ( + "**[Goal: hat-trick](https://app.clickup.com/512/goals/6)** has been updated!" + ) + self.check_webhook( + fixture_name="goal_updated", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_goal_deleted(self) -> None: + expected_message = ":trash_can: A Goal has been deleted from your ClickUp space!" + self.check_webhook( + fixture_name="goal_deleted", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_payload_with_spammy_field(self) -> None: + expected_message = ( + "**[Task: Tanswer](https://app.clickup.com/XXXXXXX/home)** has been updated!" + ) + self.check_webhook( + fixture_name="payload_with_spammy_field", + expected_topic_name=EXPECTED_TOPIC, + expected_message=expected_message, + ) + + def test_get_clickup_api_data_success_request(self) -> None: + with patch("zerver.webhooks.clickup.api_endpoints.requests.get") as mock_get: + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"key123": "value322"} + + mock_get.return_value = mock_response + + result = get_clickup_api_data("list/123123", token="123") + + mock_get.assert_called_once_with( + "https://api.clickup.com/api/v2/list/123123", + headers={ + "Content-Type": "application/json", + "Authorization": "123", + }, + params={}, + ) + self.assertEqual(result, {"key123": "value322"}) + + def test_get_clickup_api_data_failure_request(self) -> None: + with patch("zerver.webhooks.clickup.api_endpoints.requests.get") as mock_get: + mock_response = MagicMock() + mock_response.status_code = 404 + mock_get.return_value = mock_response + + exception_msg = "HTTP error accessing the ClickUp API. Error: 404" + + with self.assertRaisesRegex(Exception, exception_msg): + get_clickup_api_data("list/123123", token="123") + + mock_get.assert_called_once_with( + "https://api.clickup.com/api/v2/list/123123", + headers={ + "Content-Type": "application/json", + "Authorization": "123", + }, + params={}, + ) + + def test_get_clickup_api_data_missing_api_token(self) -> None: + with patch("zerver.webhooks.clickup.api_endpoints.requests"): + exception_msg = "ClickUp API 'token' missing in kwargs" + with self.assertRaisesRegex(AssertionError, exception_msg): + get_clickup_api_data("list/123123", asdasd="123") diff --git a/zerver/webhooks/clickup/view.py b/zerver/webhooks/clickup/view.py new file mode 100644 index 00000000000000..72a61b5101f38a --- /dev/null +++ b/zerver/webhooks/clickup/view.py @@ -0,0 +1,248 @@ +# Webhooks for external integrations. +from typing import Any +from urllib.parse import urljoin + +import requests +from django.http import HttpRequest, HttpResponse + +from zerver.decorator import webhook_view +from zerver.lib.exceptions import UnsupportedWebhookEventTypeError +from zerver.lib.response import json_success +from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint +from zerver.lib.validator import WildValue, check_none_or, check_string +from zerver.lib.webhooks.common import check_send_webhook_message, unix_milliseconds_to_timestamp +from zerver.models import UserProfile + +SIMPLE_FIELDS = ["priority", "status"] + +SPAMMY_FIELDS = ["tag", "tag_removed", "assignee_rem"] + +MESSAGE_WRAPPER = "\n~~~ quote\n {icon} {content}\n~~~\n" + +EVENT_NAME_TEMPLATE: str = "**[{event_item_type}: {event_item_name}]({item_url})**" + + +def parse_event_code(event_code: str) -> tuple[str, str]: + """ + Turns string like "taskUpdated" into ("task", "Updated") + """ + data_list = split_camel_case_string(event_code) + if len(data_list) != 2: + raise UnsupportedWebhookEventTypeError(event_code) + return data_list[0], data_list[1] + + +def split_camel_case_string(string: str) -> list[str]: + words = [] + start_index = 0 + + for i, char in enumerate(string): + if char.isupper() and i > 0: + words.append(string[start_index:i]) + start_index = i + + words.append(string[start_index:]) + + return words + + +def generate_created_event_message(item_data: dict[str, Any], event_item_type: str) -> str: + body = "\n:new: " + EVENT_NAME_TEMPLATE + " has been created in your ClickUp space!" + creator_data = item_data.get("creator") + if isinstance(creator_data, dict) and "username" in creator_data: + # Some payload only doesn't provide users data. + creator_name = creator_data["username"] + body += f"\n - Created by: **{creator_name}**" + return body.format( + event_item_type=event_item_type.title(), + event_item_name=item_data["name"], + item_url=item_data["url"], + ) + + +def generate_updated_event_message( + item_data: dict[str, Any], + event_item_type: str, + payload: WildValue, +) -> str: + body = "\n" + EVENT_NAME_TEMPLATE + " has been updated!" + history_items = payload.get("history_items", []) + + for history_dict in history_items: + updated_field = history_dict["field"].tame(check_string) + if updated_field in SPAMMY_FIELDS: + continue + elif updated_field in SIMPLE_FIELDS: + body += body_message_for_simple_fields(history_dict, event_item_type, updated_field) + else: + body += body_message_for_special_fields(history_dict, updated_field) + + return body.format( + event_item_type=event_item_type.title(), + event_item_name=item_data["name"], + item_url=item_data["url"], + ) + + +def body_message_for_simple_fields( + history_dict: WildValue, event_item_type: str, updated_field: str +) -> str: + # The value of "before"/"after" for these payloads maybe a dict or a bool + old_value = ( + history_dict.get("before").get(updated_field).tame(check_string) + if history_dict.get("before") + else None + ) + new_value = ( + history_dict.get("after").get(updated_field).tame(check_string) + if history_dict.get("after") + else None + ) + return MESSAGE_WRAPPER.format( + icon=":note:", + content=f"Updated {event_item_type} {updated_field} from **{old_value}** to **{new_value}**", + ) + + +def body_message_for_special_fields(history_dict: WildValue, updated_field: str) -> str: + event_details = history_dict.get("data", {}) + + if updated_field == "name": + old_value = history_dict["before"].tame(check_none_or(check_string)) + new_value = history_dict["after"].tame(check_none_or(check_string)) + return MESSAGE_WRAPPER.format( + icon=":pencil:", content=f"Renamed from **{old_value}** to **{new_value}**" + ) + elif updated_field == "assignee_add": + new_value = history_dict["after"]["username"].tame(check_string) + return MESSAGE_WRAPPER.format( + icon=":silhouette:", content=f"Now assigned to **{new_value}**" + ) + elif updated_field == "comment": + event_user = history_dict["user"]["username"].tame(check_string) + return MESSAGE_WRAPPER.format( + icon=":speaking_head:", content=f"Commented by **{event_user}**" + ) + elif updated_field == "due_date": + raw_old_due_date = history_dict.get("before").tame(check_none_or(check_string)) + old_due_date = ( + unix_milliseconds_to_timestamp(float(raw_old_due_date), "ClickUp").strftime("%Y-%m-%d") + if raw_old_due_date + else None + ) + raw_new_due_date = history_dict.get("after").tame(check_none_or(check_string)) + new_due_date = ( + unix_milliseconds_to_timestamp(float(raw_new_due_date), "ClickUp").strftime("%Y-%m-%d") + if raw_new_due_date + else None + ) + return MESSAGE_WRAPPER.format( + icon=":spiral_calendar:", + content=f"Due date updated from to ", + ) + elif updated_field == "section_moved": + old_value = history_dict["before"]["name"].tame(check_none_or(check_string)) + new_value = history_dict["after"]["name"].tame(check_none_or(check_string)) + return MESSAGE_WRAPPER.format( + icon=":folder:", content=f"Moved from **{old_value}** to **{new_value}**" + ) + elif updated_field == "time_spent": + raw_time_spent = event_details.get("total_time").tame(check_none_or(check_string)) + new_time_spent = ( + unix_milliseconds_to_timestamp(float(raw_time_spent), "ClickUp").strftime("%H:%M:%S") + if raw_time_spent + else None + ) + return MESSAGE_WRAPPER.format( + icon=":stopwatch:", content=f"Time spent changed to **{new_time_spent}**" + ) + elif updated_field == "time_estimate": + old_value = event_details["old_time_estimate_string"].tame(check_none_or(check_string)) + new_value = event_details["time_estimate_string"].tame(check_none_or(check_string)) + event_user = history_dict["user"]["username"].tame(check_string) + return MESSAGE_WRAPPER.format( + icon=":ruler:", + content=f"Time estimate changed from **{old_value}** to **{new_value}** by **{event_user}**", + ) + else: + raise UnsupportedWebhookEventTypeError(updated_field) + + +def get_clickup_api_data(clickup_api_path: str, **kwargs: Any) -> dict[str, Any]: + if not kwargs.get("token"): + raise AssertionError("ClickUp API 'token' missing in kwargs") + token = kwargs.pop("token") + + base_url = "https://api.clickup.com/api/v2/" + api_endpoint = urljoin(base_url, clickup_api_path) + response = requests.get( + api_endpoint, + headers={ + "Content-Type": "application/json", + "Authorization": token, + }, + params=kwargs, + ) + if response.status_code != requests.codes.ok: + raise Exception(f"HTTP error accessing the ClickUp API. Error: {response.status_code}") + return response.json() + + +def get_item_data( + event_item_type: str, api_key: str, payload: WildValue, team_id: str +) -> dict[str, Any]: + item_data: dict[str, Any] = {} + + if event_item_type not in ["task", "list", "folder", "space", "goal"]: + raise UnsupportedWebhookEventTypeError(event_item_type) + + item_id_key = f"{event_item_type}_id" + clickup_api_path = f"{event_item_type}/{payload[item_id_key].tame(check_string)}" + item_data = get_clickup_api_data(clickup_api_path, token=api_key) + + if event_item_type == "goal": + # The data for "goal" is nested one level deeper. + item_data = item_data["goal"] + + item_data["url"] = item_data.get("pretty_url", f"https://app.clickup.com/{team_id}/home") + + return item_data + + +@webhook_view("ClickUp") +@typed_endpoint +def api_clickup_webhook( + request: HttpRequest, + user_profile: UserProfile, + *, + payload: JsonBodyPayload[WildValue], + clickup_api_key: str, + team_id: str, +) -> HttpResponse: + event_code = payload["event"].tame(check_string) + event_item_type, event_action = parse_event_code(event_code=event_code) + topic = "ClickUp Notification" + + if event_action == "Deleted": + body = ( + f"\n:trash_can: A {event_item_type.title()} has been deleted from your ClickUp space!" + ) + check_send_webhook_message(request, user_profile, topic, body) + return json_success(request) + + item_data = get_item_data( + event_item_type, + clickup_api_key, + payload, + team_id, + ) + + if event_action == "Created": + body = generate_created_event_message(item_data, event_item_type) + elif event_action == "Updated": + body = generate_updated_event_message(item_data, event_item_type, payload) + else: + raise UnsupportedWebhookEventTypeError(event_code) + + check_send_webhook_message(request, user_profile, topic, body) + return json_success(request)