diff --git a/include/class-ClickUpAPI.php b/include/class-ClickUpAPI.php
index 2e95709..2075298 100644
--- a/include/class-ClickUpAPI.php
+++ b/include/class-ClickUpAPI.php
@@ -1,355 +1,360 @@
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}", [], [] );
}
/**
* 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" )->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",
"spaceDeleted",
"goalCreated",
"goalUpdated",
"goalDeleted",
"keyResultCreated",
"keyResultUpdated",
"keyResultDeleted",
];
}
}
diff --git a/include/class-PhabricatorAPI.php b/include/class-PhabricatorAPI.php
index c4327c7..89a7fd4 100644
--- a/include/class-PhabricatorAPI.php
+++ b/include/class-PhabricatorAPI.php
@@ -1,289 +1,421 @@
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 Phabricator / Phorge
*/
class PhabricatorAPI {
/**
* Query a single Phabricator Project by its slug
*
* If this is not possible, it throws an exception.
*
* @param $slug string Example 'foo_bar'
* @return mixed
*/
public static function querySingleProjectBySlug( $slug ) {
return PhabricatorAPI::querySingle( 'project.search', [
'constraints' => [
'slugs' => [ $slug ],
],
] );
}
/**
* Query a single Phabricator Project's Column by its PHID
*
* If this is not possible, it throws an exception.
*
* @param $phid string
* @return mixed
*/
public static function querySingleProjectColumnByPHID( $phid ) {
return PhabricatorAPI::querySingle( 'project.column.search', [
'constraints' => [
'phids' => [ $phid ],
],
] );
}
/**
* Try to guess a single Phabricator Project by its human name
*
* If this is not possible, NULL is returned.
*
* @param string $slug Example 'Foo Bar'
* @return mixed
*/
public static function guessPhabricatorTagFromHumanName( $name ) {
$name = str_replace( ' ', '_', $name );
$name = strtolower( $name );
$project = null;
try {
$project = self::querySingleProjectBySlug( $name );
} catch( Exception $e ) {
// do nothing
}
return $project;
}
public static function query( $entry_point, $query = [] ) {
$client = new ConduitClient( PHABRICATOR_URL );
$client->setConduitToken( PHABRICATOR_CONDUIT_API_TOKEN );
return $client->callMethodSynchronous( $entry_point, $query );
}
/**
* Simplified query to the maniphest.edit Conduit Phabricator API
*
* https://sviluppo.erinformatica.it/conduit/method/maniphest.edit/
*
* @param $transaction_values array Example: [ 'title' => 'ASD' ]
* @param $extra_query array Example: ['objectIdentifier' => 'PHID--...'] to edit
* @return mixed
*/
public static function createTask( $transaction_values = [], $query = [] ) {
// build transactions
$query['transactions'] = [];
foreach( $transaction_values as $key => $value ) {
$query['transactions'][] = self::transaction( $key, $value );
}
return self::query( 'maniphest.edit', $query );
}
/**
* Simplified query to the maniphest.edit Conduit Phabricator API
*
* https://sviluppo.erinformatica.it/conduit/method/maniphest.edit/
*
* @param $id string Task ID (e.g. 123 for T123)
* @param $transaction_values array Example: [ 'title' => 'ASD' ]
* @return mixed
*/
public static function editTask( $id, $transaction_values = [] ) {
return self::createTask( $transaction_values, [
'objectIdentifier' => $id,
] );
}
/**
* Check if a Phabricator status is equivalent to "closed"
*
* @return self
*/
public static function isStatusClosed( $task_status ) {
// TODO: read from "maniphest.status.search" and set in cache and read from there
$closed_statuses = [
'resolved',
'wontfix',
'invalid',
'duplicate',
'spite',
];
return in_array( $task_status, $closed_statuses, true );
}
/**
* Convert a Phabricator status to a Clickup status
*
* @return "closed" or "open"
*/
public static function status2clickup( $task_status ) {
$is_closed = self::isStatusClosed( $task_status );
if( $is_closed ) {
return 'closed';
}
return 'open';
}
/**
* Get a Task by its ID
*
* It returns just one element.
*
* https://sviluppo.erinformatica.it/conduit/method/maniphest.search/
*
* @param $task_id mixed
*/
public static function getTask( $task_id ) {
$task_id = self::sanitize_task_id( $task_id );
$query = [
'constraints' => [
'ids' => [ $task_id ],
],
'attachments' => [
'columns' => true,
'projects' => true,
],
];
return self::searchSingleTask( $query );
}
public static function getUserByPHID( $phid ) {
$query = [
'constraints' => [
'phids' => [ $phid ],
],
];
return self::querySingle( 'user.search', $query );
}
public static function getUserByUsername( $username ) {
$query = [
'constraints' => [
'usernames' => [ $username ],
],
];
return self::querySingle( 'user.search', $query );
}
public static function getTaskByPHID( $phid ) {
$query = [
'constraints' => [
'phids' => [ $phid ],
],
'attachments' => [
'columns' => true,
'projects' => true,
],
];
return self::searchSingleTask( $query );
}
public static function searchSingleUserByPHID( $phid ) {
$query = [
'constraints' => [
'phids' => [ $phid ],
],
];
return self::searchSingleUser( $query );
}
public static function searchSingleTask( $query ) {
return self::querySingle( 'maniphest.search', $query );
}
public static function searchSingleUser( $query ) {
return self::querySingle( 'user.search', $query );
}
/**
* Return a single element from Phabricator or throw an exception
*
* @param $method string
* @param $query array
* @return mixed
*/
public static function querySingle( $method, $query ) {
$results = self::query( $method, $query );
// just the first one is OK
foreach( $results['data'] as $entry ) {
return $entry;
}
throw new Exception( "Phabricator result not found from $method using: " . json_encode( $query ) );
}
public static function searchObjectTransactionsFromTransactions( $phab_object_id, $transactions ) {
$transaction_phids = [];
foreach( $transactions as $transaction ) {
$transaction_phids[] = $transaction['phid'];
}
return self::searchObjectTransactionsFromPHIDs( $phab_object_id, $transaction_phids );
}
public static function searchObjectTransactionsFromPHIDs( $phab_object_id, $transaction_phids ) {
$query = [
'objectIdentifier' => $phab_object_id,
'constraints' => [
'phids' => $transaction_phids,
],
];
$results = self::query( 'transaction.search', $query );
return $results['data'];
}
private static function transaction( $type, $value ) {
return [
'type' => $type,
'value' => $value,
];
}
/**
* Sanitize a Task ID
*
* @param $task_id mixed
* @return int
*/
public static function sanitize_task_id( $task_id ) {
// strip the damn 'T' since the 'id' API only accepts numeric
$task_id = ltrim( $task_id, 'T' );
// no numeric no party
$task_id = (int)$task_id;
if( !$task_id ) {
throw new Exception( "invalid Task ID" );
}
return $task_id;
}
+ /**
+ * Try to create (or update) a Phabricator Task from a ClickUp Task ID
+ *
+ * @param $clickup_task_id string
+ * @return mixed
+ */
+ public static function createOrUpdateTaskFromClickUpTaskID( $clickup_task_id ) {
+
+ $phab_task = null;
+
+ // proposed changeset for a Phabricator task (to update or create)
+ $phab_task_args = [];
+
+ $cache = ClickUpPhabricatorCache::instance();
+
+ // get fresh ClickUp Task data
+ $clickup_task = ClickUpAPI::queryTaskData( $clickup_task_id );
+
+ // TODO: move in the QueryTaskData some validation on these
+ $clickup_task_title = $clickup_task->name;
+ $clickup_task_descr = $clickup_task->description;
+ $clickup_task_folder = $clickup_task->folder;
+ $clickup_task_folder_id = $clickup_task_folder->id;
+ $clickup_task_folder_name = $clickup_task_folder->name;
+
+ // unuseful checks
+ if( !$clickup_task_title ) {
+ throw new Exception( "missing title in new ClickUp Task" );
+ }
+
+ // check the ClickUp assignees on this Task
+ $clickup_assignes = $clickup_task->assignees;
+ foreach( $clickup_assignes as $clickup_assigne ) {
+
+ // find a Task owner for Phabricator
+ $phab_assignee_phid = $cache->getClickUpUserPHIDByID( $clickup_assigne->id );
+ if( $phab_assignee_phid ) {
+
+ // one is enough
+ $phab_task_args['owner'] = $phab_assignee_phid;
+ break;
+ }
+ }
+
+ // check if this ClickUp task is already known by Phabricator
+ $phab_task_id = $cache->getClickupTaskPhabricatorID( $clickup_task_id );
+ if( $phab_task_id ) {
+
+ $phab_task = PhabricatorAPI::getTask( $phab_task_id );
+
+ // status of the Phabricator Task
+ $phab_task_status = $phab_task['fields']['status']['value'];
+
+ // status of the ClickUp Task
+ $clickup_task_status = $clickup_task->status->type;
+
+ // status of the ClickUp Task translated for Phabricator
+ $clickup_task_status_for_phab = ClickUpAPI::taskStatusTypeToPhabricator( $clickup_task_status );
+
+ // check the respective openess status
+ $clickup_task_is_closed = ClickupAPI::isStatusClosed( $clickup_task_status );
+ $phab_task_is_closed = PhabricatorAPI::isStatusClosed( $phab_task_status );
+
+ // verify that the generical status changed
+ if( $clickup_task_is_closed !== $phab_task_is_closed ) {
+
+ // verify that the specific status will change
+ if( $phab_task_status !== $clickup_task_status_for_phab ) {
+
+ // update the status of the Phabricator Task
+ $phab_task_args['status'] = $clickup_task_status_for_phab;
+ }
+ }
+
+ // probably Phabricator already created it from the Phabricator webhook
+ error_log( "updating existing ClickUp Task $clickup_task_id with Phabricator Task $phab_task_id" );
+
+ // update the Phabricator Task if there is something to be updated
+ if( $phab_task_args ) {
+ error_log("trying to edit task $phab_task_id (SKIP DEBUG) with: " . json_encode( $phab_task_args ) );
+// PhabricatorAPI::editTask( $phab_task_id, $phab_task_args );
+ }
+
+ } else {
+
+ // probably Phabricator already created it from the Phabricator webhook
+ error_log( "creating ClickUp Task $clickup_task_id in Phabricator" );
+
+ // let's create a new Task in Phabricator
+
+ // 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";
+
+ // 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 {
+ // whops
+ error_log( "unable to recognize the Phabricator Tag for Task $clickup_task_id - please run ./cli/clickup-folders.php" );
+
+ // 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 = PhabricatorAPI::createTask( $phab_task_args );
+ $phab_task_phid = $phab_task['object']['phid']; // TASK-PHID-00000000
+ $phab_task_id = $phab_task['object']['id']; // 123 for T123
+
+ // associate the Phabricator Task to the Conduit Task in the cache
+ $cache
+ ->lock()
+ ->registerClickupPhabricatorTaskID( $clickup_task_id, $phab_task_id )
+ ->commit();
+
+ // update Click-Up adding the Phabricator URL :D
+ $phab_task_url = PHABRICATOR_URL . "T{$phab_task_id}";
+ ClickUpAPI::putTaskData( $clickup_task_id, [], [
+ 'description' => "$clickup_task_descr\n\n$phab_task_url",
+ ] );
+ }
+
+ return $phab_task;
+ }
+
}
diff --git a/public/webhook-clickup/index.php b/public/webhook-clickup/index.php
index 02e4213..1bcea4b 100644
--- a/public/webhook-clickup/index.php
+++ b/public/webhook-clickup/index.php
@@ -1,218 +1,222 @@
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 .
/**
* Receive ClickUp webhooks
*
* Documentation:
* https://clickup.com/api/developer-portal/webhooktaskpayloads/
*/
// default response code
http_response_code( 500 );
require realpath( __DIR__ ) . '/../autoload.php';
$response = file_get_contents('php://input');
$response_data = json_decode( $response );
-$event = $response_data->event ?? null;
-$clickup_task_id = $response_data->task_id ?? null;
+$event = $response_data->event ?? null;
+$clickup_task_id = $response_data->task_id ?? null;
// find history items (and the first one)
$history_items = $response_data->history_items ?? [];
$history_item_first = null;
foreach( $history_items as $history_item_first ) {
break;
}
// no signature no party
$signature_header = $_SERVER['HTTP_X_SIGNATURE'] ?? null;
if( !$signature_header ) {
http_response_code( 400 );
echo "This is not a request from an official ClickUp webhook.\n";
exit;
}
// do the math
$signature_calculated = hash_hmac( 'sha256', $response, CLICKUP_WEBHOOK_HMAC_KEY );
if( $signature_calculated !== $signature_header ) {
http_response_code( 400 );
throw new Exception( "invalid signature, calculated $signature_calculated expected $signature_header" );
exit;
}
$cache = ClickUpPhabricatorCache::instance();
switch( $event ) {
case 'taskCreated':
// check if we already know about it
$phab_task_id = $cache->getClickupTaskPhabricatorID( $clickup_task_id );
if( $phab_task_id ) {
// probably Phabricator already created it from the Phabricator webhook
error_log( "skip new ClickUp Task $clickup_task_id: already imported in Phab as $phab_task_id" );
} else {
// let's create the Task
// 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";
}
// try to assign to an user
$clickup_assignes = $clickup_task_details->assignees;
foreach( $clickup_assignes as $clickup_assigne ) {
$phab_assignee_phid = $cache->getClickUpUserPHIDByID( $clickup_assigne->id );
if( $phab_assignee_phid ) {
$phab_task_args['owner'] = $phab_assignee_phid;
break;
}
}
$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
->lock()
->registerClickupPhabricatorTaskID( $clickup_task_id, $phab_task_id )
->commit();
// 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':
// if it's deleted in ClickUp, close as invalid in Phabricator
$phab_task_id = $cache->getClickupTaskPhabricatorID( $clickup_task_id );
if( $phab_task_id ) {
PhabricatorAPI::editTask( $phab_task_id, [
'status' => 'invalid',
] );
}
// drop ClickUp Task from internal cache
$cache->removeClickupTask( $clickup_task_id );
break;
+ case 'taskUpdated':
+ PhabricatorAPI::createOrUpdateTaskFromClickUpTaskID( $clickup_task_id );
+ break;
+
case 'taskCommentPosted':
// post the comment on Phabricator too
$phab_task_id = $cache->getClickupTaskPhabricatorID( $clickup_task_id );
if( $phab_task_id && $history_item_first ) {
$clickup_comment_author_name = $history_item_first->user->username;
$clickup_comment_author_id = $history_item_first->user->id;
$clickup_task_description = $history_item_first->comment->text_content;
// avoid to post on Phabricator, comments that are coming from our Phabricator bot
if( strpos( $clickup_task_description, COMMENT_PREFIX_FROM_PHABRICATOR ) === 0 ) {
error_log( "skip ClickUp comment that was posted by the Phabricator bot" );
} else {
// add a nice comment to Phabricator
$phab_comment = "From **{$clickup_comment_author_name}**:\n\n" . $clickup_task_description;
$phab_task_data = PhabricatorAPI::editTask( $phab_task_id, [
'comment' => $phab_comment,
] );
}
}
break;
case 'taskStatusUpdated':
// check i the Phabricator Task was already connected to this ClickUp Task
$phab_task_id = $cache->getClickupTaskPhabricatorID( $clickup_task_id );
if( $phab_task_id && $history_item_first ) {
// fetch fresh Phabricator Task data
$phab_task = PhabricatorAPI::getTask( $phab_task_id );
$phab_task_status = $phab_task['fields']['status']['value'];
$clickup_task_status = $history_item_first->data->status_type;
$clickup_task_status_for_phab = ClickUpAPI::taskStatusTypeToPhabricator( $clickup_task_status );
// check if Phabricator has a different Task status than ClickUp
$clickup_task_is_closed = ClickupAPI::isStatusClosed( $clickup_task_status );
$phab_task_is_closed = PhabricatorAPI::isStatusClosed( $phab_task_status );
if( $clickup_task_is_closed !== $phab_task_is_closed ) {
// update Phab only if the status changes
if( $phab_task_status !== $clickup_task_status_for_phab ) {
error_log( "ClickUp Task $clickup_task_id status $clickup_task_status (-> $clickup_task_status_for_phab) - Phab Task $phab_task_id status: $phab_task_status - updating" );
// set the ClickUp Task status in Phabricator as well
PhabricatorAPI::editTask( $phab_task_id, [
'status' => $clickup_task_status_for_phab,
] );
}
} else {
error_log( "ClickUp skip status $clickup_task_status (Phab was $phab_task_status)" );
}
}
break;
default:
echo "Method not supported.\n";
error_log( "unsupported ClickUp webhoook: $event" );
}
// success
http_response_code( 200 );