diff --git a/cli/clickup-webhook-delete.php b/cli/clickup-webhook-delete.php
new file mode 100755
index 0000000..c04bc8a
--- /dev/null
+++ b/cli/clickup-webhook-delete.php
@@ -0,0 +1,28 @@
+#!/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
+ */
+
+// autoload libraries
+require __DIR__ . '/../autoload.php';
+
+$result = ClickUpAPI::deleteWebhook();
+
+print_r( $result );
diff --git a/cli/clickup-webhook-list.php b/cli/clickup-webhook-list.php
new file mode 100755
index 0000000..f65cd62
--- /dev/null
+++ b/cli/clickup-webhook-list.php
@@ -0,0 +1,30 @@
+#!/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 .
+
+/**
+ * Register the ClickUp webhook to receive info
+ */
+
+// autoload libraries
+require __DIR__ . '/../autoload.php';
+
+// yeah
+$webhooks = ClickUpAPI::queryWebhooks();
+foreach( $webhooks as $webhook ) {
+ print_r( $webhook );
+}
diff --git a/cli/clickup-webhook-register.php b/cli/clickup-webhook-register.php
new file mode 100755
index 0000000..5bba1a7
--- /dev/null
+++ b/cli/clickup-webhook-register.php
@@ -0,0 +1,27 @@
+#!/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 .
+
+/**
+ * Register the ClickUp webhook to receive info
+ */
+
+// autoload libraries
+require __DIR__ . '/../autoload.php';
+
+// yeah
+ClickUpAPI::registerWebhook();
diff --git a/cli/clickup-webhook-update.php b/cli/clickup-webhook-update.php
new file mode 100755
index 0000000..ebec265
--- /dev/null
+++ b/cli/clickup-webhook-update.php
@@ -0,0 +1,28 @@
+#!/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
+ */
+
+// autoload libraries
+require __DIR__ . '/../autoload.php';
+
+$result = ClickUpAPI::updateWebhook();
+
+print_r( $result );
diff --git a/config-example.php b/config-example.php
index e49f84b..66ed01e 100644
--- a/config-example.php
+++ b/config-example.php
@@ -1,53 +1,67 @@
ClickUp bot
-# Copyright (C) 2022 Valerio Bozzolan, contributors
+# 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 .
// ClickUp Team ID - you can find it from your home URL
// Example: "123" if your home URL is "https://app.clickup.com/123/v/l/li/456456456456"
define( 'CLICKUP_TEAM_ID', 123 );
+// URL exposing the "clickup-webhook" directory from this repository
+// NOTE: this MUST be a valid and public URL so ClickUp can use that as webhook
+// NOTE: the checkorigin token is just a random token you should invent
+define( 'CLICKUP_WEBHOOK_PUBLIC_ENDPOINT', 'https://example.com/clickup-phabricator-phorge-bot/clickup-webhook/?checkorigintoken=abcdefghilmpopqrstuvz123' );
+
// obtained after registering the webhook using register-webhook.php
define( 'CLICKUP_REGISTERED_WEBHOOK_ID', '123-123-123-123-123' );
+// keep this in sync with the random token you invented in CLICKUP_WEBHOOK_PUBLIC_ENDPOINT
+define( 'CLICKUP_WEBHOOK_CHECKORIGINTOKEN', 'abcdefghilmpopqrstuvz123' );
+
+// when true, CLICKUP_WEBHOOK_PUBLIC_ENDPOINT is compared to the one in CLICKUP_WEBHOOK_PUBLIC_ENDPOINT
+define( 'CLICKUP_VALIDATE_WEBHOOK', true );
+
// https://app.clickup.com/36232585/settings/apps
define( 'CLICKUP_API_TOKEN', 'pk_123_ASD123' );
// ClickUp cache (connecting its stuff to Phabricator, avoid creating duplicate tasks, etc.)
define( 'CLICKUP_CACHE_JSON_FILE', __DIR__ . '/data/cache.json' );
// ClickUp cache pretty print
define( 'CLICKUP_CACHE_JSON_PRETTY', true );
// Phabricator home URL (to call API)
// MUST end with a slash
define( 'PHABRICATOR_URL', 'https://sviluppo.erinformatica.it/' );
// path to the Arcanist library of your Phabricator installation (to call APIs)
define( 'PHABRICATOR_ARCANIST_PATH', '/var/www/phabricator/arcanist/support/init/init-script.php' );
// Phabricator Conduit API token
// at the moment it's in use the one from the user er.clickup.bot in ER Phabricator
// https://sviluppo.erinformatica.it/p/er.clickup.bot/
// To generate a new one:
// https://sviluppo.erinformatica.it/conduit/token/edit/15/
define( 'PHABRICATOR_CONDUIT_API_TOKEN', 'api-asd123' );
-// not supported already :D sorry
-define( 'CLICKUP_VALIDATE_WEBHOOK', false );
+// validate the exact webhook URL
+// if it does not match it does nothing
+// this is useful if you put some very weird and secret query strings here:
+// CLICKUP_WEBHOOK_PUBLIC_ENDPOINT
+define( 'CLICKUP_VALIDATE_WEBHOOK', true );
// homepage of the project
// just to know where you can send bugs
define( 'REPOSITORY_URL', 'https://gitpull.it/source/clickup-phabricator-bot/' );
diff --git a/include/class-ClickUpAPI.php b/include/class-ClickUpAPI.php
index ed7f288..c77aee1 100644
--- a/include/class-ClickUpAPI.php
+++ b/include/class-ClickUpAPI.php
@@ -1,156 +1,254 @@
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 ?? [];
}
/**
* https://clickup.com/api/clickupreference/operation/GetSpaces/
*
* @return array
*/
public static function getSpaceFolders( $space_id ) {
return self::querySpaceFolders( $space_id )->folders ?? [];
}
/**
* https://clickup.com/api/clickupreference/operation/GetTask/
*/
public static function queryTaskData( $task_id ) {
// append the Team ID since it does not work otherwise
$task_details = self::requestGET( "/task/$task_id", self::query_team() );
// no Task no party
if( !$task_details ) {
throw new Exception( "missing ClickUp Task $task_id" );
}
// save some stuff in the cache
ClickUpPhabricatorCache::instance()
->newlock()
->importClickupTaskData( $task_id, $task_details )
->save();
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()
->newlock()
->importClickupTaskData( $task_id, $task_details )
->save();
return $task_details;
}
+ /**
+ * 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 );
+ }
+
+ /**
+ * 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;
+ }
+ $team_id = CLICKUP_TEAM_ID;
+ return self::request( "DELETE", "/team/{$team_id}/webhook/{$webhook_id}", [], [] );
+ }
+
+ /**
+ * https://clickup.com/api/clickupreference/operation/UpdateWebhook/
+ */
+ public static function updateWebhook( $webhook_id = null, $endpoint = null, $events = [] ) {
+ if( !$webhook_id ) {
+ $webhook_id = CLICKUP_REGISTERED_WEBHOOK_ID;
+ }
+ if( !$endpoint ) {
+ $endpoint = CLICKUP_WEBHOOK_PUBLIC_ENDPOINT;
+ }
+ $payload = [
+ 'endpoint' => $endpoint,
+ ];
+ return self::requestPUT( "/webhook/{$webhook_id}", [], $payload );
+ }
+
/**
* 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" );
}
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 ) ) {
throw new Exception( sprintf(
"ClickUp API error: %s",
json_encode( $data )
) );
}
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",
+ "spaceDeleted",
+ "goalCreated",
+ "goalUpdated",
+ "goalDeleted",
+ "keyResultCreated",
+ "keyResultUpdated",
+ "keyResultDeleted",
+ ];
+ }
+
}
diff --git a/public/clickup-webhook/index.php b/public/clickup-webhook/index.php
index f80be04..d98a4f5 100644
--- a/public/clickup-webhook/index.php
+++ b/public/clickup-webhook/index.php
@@ -1,114 +1,123 @@
event ?? null;
$clickup_task_id = $response_data->task_id ?? null;
$webhook_id = $_SERVER['HTTP_X_SIGNATURE'] ?? null;
// check incoming request
-if( CLICKUP_VALIDATE_WEBHOOK && $webhook_id !== CLICKUP_REGISTERED_WEBHOOK_ID ) {
- echo "Unknown webhook signature\n";
- throw new Exception( sprintf( "unknown webhook ID: %s", $webhook_id ) );
+if( CLICKUP_VALIDATE_WEBHOOK ) {
+
+ // TODO
+ //if( $webhook_id !== CLICKUP_REGISTERED_WEBHOOK_ID ) {
+ // throw new Exception( sprintf( "unknown webhook ID: %s", $webhook_id ) );
+ //}
+
+ $checkorigintoken = $_GET['checkorigintoken'] ?? null;
+ if( $checkorigintoken !== CLICKUP_WEBHOOK_CHECKORIGINTOKEN ) {
+ echo "Invalid check origin token\n";
+ exit;
+ }
}
$cache = ClickUpPhabricatorCache::instance();
switch( $event ) {
case 'taskCreated':
// get more ClickUp info (title and description etc.) and remember in cache
$clickup_task_details = ClickUpAPI::queryTaskData( $clickup_task_id );
// TODO: move in the QueryTaskData some validation on these
$clickup_task_title = $clickup_task_details->name;
$clickup_task_descr = $clickup_task_details->description;
$clickup_task_folder = $clickup_task_details->folder;
$clickup_task_folder_id = $clickup_task_folder->id;
$clickup_task_folder_name = $clickup_task_folder->name;
if( !$clickup_task_title ) {
throw new Exception( "missing title in new ClickUp Task" );
}
// prepare description for new Task in Phabricator
$phab_task_description = $clickup_task_descr;
$phab_task_description .= "\n\n";
$phab_task_description .= "> https://app.clickup.com/t/$clickup_task_id";
$phab_task_args = [];
// try to assign to the right project
$phab_task_project = $cache->getPhabricatorTagIDFromClickupFolderID( $clickup_task_folder_id );
if( $phab_task_project ) {
// assign to the right project
$phab_task_args['projects.add'] = [ $phab_task_project ];
} else {
// or just give an hint
$clickup_task_title = "[$clickup_task_folder_name] $clickup_task_title";
}
$phab_task_args['title'] = $clickup_task_title;
$phab_task_args['description'] = $phab_task_description;
// create Task in Phabricator
$phab_task_data = PhabricatorAPI::createTask( $phab_task_args );
$phab_task_phid = $phab_task_data['object']['phid']; // TASK-PHID-00000000
$phab_task_id = $phab_task_data['object']['id']; // 123 for T123
// associate the Phabricator Task to the Conduit Task in the cache
$cache->newlock()->registerClickupPhabricatorTaskID( $clickup_task_id, $phab_task_id )->save();
// update Click-Up adding the Phabricator URL
$phab_task_url = PHABRICATOR_URL . "T{$phab_task_id}";
ClickUpAPI::putTaskData( $clickup_task_id, [], [
'description' => "$clickup_task_descr\n\n$phab_task_url",
] );
break;
case 'taskDeleted':
// mark as invalid in Phabricator
$phab_task_id = $cache->getClickupTaskPhabricatorID( $clickup_task_id );
if( $phab_task_id ) {
PhabricatorAPI::editTask( $phab_task_id, [
'status' => 'invalid',
] );
}
// drop from cache
$cache->removeClickupTask( $clickup_task_id );
break;
case 'taskCommentPosted':
// post the comment on Phabricator too
$phab_task_id = $cache->getClickupTaskPhabricatorID( $clickup_task_id );
if( $phab_task_id ) {
$phab_comment = "Test:\n\n";
$phab_task_data = PhabricatorAPI::editTask( $phab_task_id, [
'comment' => $phab_comment,
] );
}
break;
// case 'taskUpdated':
// break;
default:
echo "Method not supported.\n";
error_log( "unsupported ClickUp webhoook: $event" );
}
$cache->save();