diff --git a/cli/clickup-webhook-update.php b/cli/clickup-webhook-update.php
index ebec265..c86f0a8 100755
--- a/cli/clickup-webhook-update.php
+++ b/cli/clickup-webhook-update.php
@@ -1,28 +1,31 @@
#!/usr/bin/php
ClickUp bot
# Copyright (C) 2023 Valerio Bozzolan, contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
/**
- * Delete a previously-registered webhook from ClickUp
+ * Update an already-registered webhook from ClickUp
+ *
+ * For example to apply a new CLICKUP_WEBHOOK_PUBLIC_ENDPOINT
+ * from your config, or to update the registered events.
*/
// autoload libraries
require __DIR__ . '/../autoload.php';
$result = ClickUpAPI::updateWebhook();
print_r( $result );
diff --git a/include/class-ClickUpAPI.php b/include/class-ClickUpAPI.php
index 2075298..ad07990 100644
--- a/include/class-ClickUpAPI.php
+++ b/include/class-ClickUpAPI.php
@@ -1,360 +1,369 @@
ClickUp bot
# Copyright (C) 2023 Valerio Bozzolan, contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
/**
* Utility to run HTTP queries against ClickUp
*/
class ClickUpAPI {
/**
* https://clickup.com/api/clickupreference/operation/GetSpaces/
*
* @return array
*/
public static function getSpaces() {
return self::querySpaces()->spaces ?? [];
}
/**
* Get all the known space folders
*
* It also saves some info in the cache
*
* https://clickup.com/api/clickupreference/operation/GetSpaces/
*
* @return array
*/
public static function getSpaceFolders( $space_id ) {
// request
$folders = self::querySpaceFolders( $space_id );
// import results
$cache = ClickUpPhabricatorCache::instance();
$cache->lock();
foreach( $folders as $folder ) {
$cache->importClickupFolder( $folder );
}
$cache->commit();
// expose
return $folders;
}
/**
* https://clickup.com/api/clickupreference/operation/GetTask/
*/
public static function queryTaskData( $task_id ) {
// no Task no party
if( !$task_id ) {
throw new Exception( "missing Task ID" );
}
try {
// try to fetch fresk ClickUp Task info
$task_details = self::requestGET( "/task/$task_id", self::query_team() );
} catch( ClickupExceptionTaskNotFound $e ) {
// drop unuseful stuff from the cache
ClickUpPhabricatorCache::instance()
->lock()
->removeClickupTask( $task_id )
->commit();
return false;
}
// save some stuff in the cache
ClickUpPhabricatorCache::instance()
->lock()
->importClickupTaskData( $task_id, $task_details )
->commit();
return $task_details;
}
/**
* https://clickup.com/api/clickupreference/operation/UpdateTask/
*/
public static function putTaskData( $task_id, $query = [], $payload = [] ) {
$task_details = self::requestPUT( "/task/$task_id", $query, $payload );
ClickUpPhabricatorCache::instance()
->lock()
->importClickupTaskData( $task_id, $task_details )
->commit();
return $task_details;
}
/**
* https://clickup.com/api/clickupreference/operation/CreateTask/
*/
public static function createTaskOnList( $list_id, $payload = [] ) {
$task_details = self::requestPOST( "/list/$list_id", [], $payload );
ClickUpPhabricatorCache::instance()
->lock()
->importClickupTaskData( $task_details->id, $task_details )
->commit();
return $task_details;
}
/**
* Convert a ClickUp status to a Phabricator one
*
* Note that ClickUp has a "type" and a "status".
*
* @return boolean
*/
public static function isStatusClosed( $type, $status = null ) {
if( $type === 'closed' ) {
return true;
}
if( $type === 'open' ) {
return false;
}
// "In progress" etc.
return false;
}
/**
* Convert a ClickUp status to a Phabricator one
*
* Note that ClickUp has a "type" and a "status".
*
* @return string
*/
public static function taskStatusTypeToPhabricator( $type, $status = null ) {
if( $type === 'closed' ) {
return 'resolved';
}
if( $type === 'open' ) {
return $type;
}
if( $type === 'custom' ) {
if( $status === 'in progress' ) {
return 'doing';
}
}
return $status ?? $type;
}
/**
* Register the ClickUp webhook
*/
public static function registerWebhook( $endpoint = null, $events = null ) {
// assume the default endpoint
if( !$endpoint) {
$endpoint = CLICKUP_WEBHOOK_PUBLIC_ENDPOINT;
}
// assume all events
if( !$events ) {
$events = self::default_webhook_events();
}
$payload = [
"endpoint" => $endpoint,
"events" => $events,
];
$team_id = CLICKUP_TEAM_ID;
return self::requestPOST( "/team/$team_id/webhook", [], $payload );
}
- /**
- * Add comment to a Task
- *
- * https://clickup.com/api/clickupreference/operation/CreateTaskComment/
- */
- public static function addTaskComment( $task_id, $comment_text ) {
- $payload = [
- 'comment_text' => $comment_text,
-// 'assignee' => 123,
- ];
- return self::requestPOST( "/task/$task_id/comment", [], $payload );
- }
-
- /**
- * https://clickup.com/api/clickupreference/operation/GetWebhooks/
- */
- public static function queryWebhooks() {
- $team_id = CLICKUP_TEAM_ID;
- $response = self::requestGET( "/team/{$team_id}/webhook", [], [] );
- return $response->webhooks;
- }
-
/**
* https://clickup.com/api/clickupreference/operation/DeleteWebhook/
*/
public static function deleteWebhook( $webhook_id = null ) {
if( !$webhook_id ) {
$webhook_id = CLICKUP_REGISTERED_WEBHOOK_ID;
}
return self::request( "DELETE", "/webhook/{$webhook_id}", [], [] );
}
/**
+ * Update an already-defined ClickUp webhook
+ *
+ * @param $webhook_id string
+ * @param $endpoint string
+ * @param $events array
* https://clickup.com/api/clickupreference/operation/UpdateWebhook/
*/
- public static function updateWebhook( $webhook_id = null, $endpoint = null, $events = [] ) {
+ public static function updateWebhook( $webhook_id = null, $endpoint = null, $events = null ) {
if( !$webhook_id ) {
$webhook_id = CLICKUP_REGISTERED_WEBHOOK_ID;
}
if( !$endpoint ) {
$endpoint = CLICKUP_WEBHOOK_PUBLIC_ENDPOINT;
}
+ if( !$events ) {
+ $events = self::default_webhook_events();
+ }
$payload = [
'endpoint' => $endpoint,
+ 'events' => $events,
];
return self::requestPUT( "/webhook/{$webhook_id}", [], $payload );
}
+ /**
+ * Add comment to a Task
+ *
+ * https://clickup.com/api/clickupreference/operation/CreateTaskComment/
+ */
+ public static function addTaskComment( $task_id, $comment_text ) {
+ $payload = [
+ 'comment_text' => $comment_text,
+// 'assignee' => 123,
+ ];
+ return self::requestPOST( "/task/$task_id/comment", [], $payload );
+ }
+
+ /**
+ * https://clickup.com/api/clickupreference/operation/GetWebhooks/
+ */
+ public static function queryWebhooks() {
+ $team_id = CLICKUP_TEAM_ID;
+ $response = self::requestGET( "/team/{$team_id}/webhook", [], [] );
+ return $response->webhooks;
+ }
+
/**
* https://clickup.com/api/clickupreference/operation/GetSpaces/
*
* @return mixed
*/
private static function querySpaces() {
$team_id = CLICKUP_TEAM_ID;
return self::requestGET( "/team/$team_id/space" );
}
/**
* https://clickup.com/api/clickupreference/operation/GetFolders/
*/
private static function querySpaceFolders( $space_id ) {
$team_id = CLICKUP_TEAM_ID;
return self::requestGET( "/space/$space_id/folder" )->folders;
}
private static function request( $method, $path, $query = [], $payload = [] ) {
$API_TOKEN = CLICKUP_API_TOKEN;
$url = "https://api.clickup.com/api/v2" . $path;
$full_url = $url . '?' . http_build_query( $query );
$curl_opts = [
CURLOPT_HTTPHEADER => [
"Authorization: $API_TOKEN",
"Content-Type: application/json"
],
CURLOPT_URL => $full_url,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
];
// PUT requests
if( $payload ) {
$payload_raw = json_encode( $payload );
$curl_opts[CURLOPT_POSTFIELDS] = $payload_raw;
}
$curl = curl_init();
curl_setopt_array( $curl, $curl_opts );
$response = curl_exec( $curl );
$error = curl_error( $curl );
curl_close( $curl );
if( $error ) {
throw new Exception( "cURL Error ClickUp #:" . $error );
}
$data = json_decode( $response );
if( $data === false ) {
throw new Exception( "cannot parse JSON of ClickUp response" );
}
// formal error from ClickUp
if( isset( $data->err ) ) {
// spawn a nice exact exception if possible
switch( $data->ECODE ) {
case 'ITEM_013':
throw new ClickupExceptionTaskNotFound( $data->err );
default:
throw new Exception( sprintf(
"ClickUp API error: %s from URL: %s payload: %s",
json_encode( $data ),
$full_url,
json_encode( $payload )
) );
}
}
return $data;
}
public static function requestGET( $path, $query = [], $payload = [] ) {
return self::request( "GET", $path, $query, $payload );
}
public static function requestPUT( $path, $query, $payload = [] ) {
return self::request( "PUT", $path, $query, $payload );
}
public static function requestPOST( $path, $query, $payload = [] ) {
return self::request( "POST", $path, $query, $payload );
}
private static function query_team( $query = [] ) {
$query[ 'team_id' ] = CLICKUP_TEAM_ID;
return $query;
}
private static function default_webhook_events() {
return [
"taskCreated",
"taskUpdated",
"taskDeleted",
"taskPriorityUpdated",
"taskStatusUpdated",
"taskAssigneeUpdated",
"taskDueDateUpdated",
"taskTagUpdated",
"taskMoved",
"taskCommentPosted",
"taskCommentUpdated",
"taskTimeEstimateUpdated",
"taskTimeTrackedUpdated",
"listCreated",
"listUpdated",
"listDeleted",
"folderCreated",
"folderUpdated",
"folderDeleted",
"spaceCreated",
- "spaceUpdated",
+// "spaceUpdated", // this is nonsense since every time you visit a space it fires
"spaceDeleted",
"goalCreated",
"goalUpdated",
"goalDeleted",
"keyResultCreated",
"keyResultUpdated",
"keyResultDeleted",
];
}
}