diff --git a/include/class-ClickUpAPI.php b/include/class-ClickUpAPI.php
index 1284f02..64a2e17 100644
--- a/include/class-ClickUpAPI.php
+++ b/include/class-ClickUpAPI.php
@@ -1,421 +1,603 @@
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;
}
/**
* Get the ClickUp Task data from cache
* If this is not possible, it's fecthed from an API query
* @param $click_id string
* @return object
*/
public static function getTaskDataByID( $click_id ) {
// try to get from the cache
$cache = ClickUpPhabricatorCache::instance();
$task_data = $cache->findClickupTaskByID( $click_id );
if( !$task_data ) {
// try to fecth from API (it updates the cache)
self::queryTaskData( $click_id );
}
// get the fresh copy from the cache (or a weird object with NULL fields)
return $cache->getClickupTask( $click_id );
}
/**
* Find a ClickUp top parent, from a ClickUp Task ID
*/
public static function findTopParentOfTaskID( $click_id, $tried = [] ) {
// loop detector
if( in_array( $click_id, $tried, true ) ) {
error_log( "loop detected in ClickUp Task $click_id" );
return null;
}
$task = self::getTaskDataByID( $click_id );
if( $task->parent ) {
$tried[] = $click_id;
$task = self::findTopParentOfTaskID( $task->parent, $tried );
}
return $task;
}
/**
* Execute an API query to fecth a fresh result
* 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;
} catch ( ClickupExceptionAccessDenied $e ) {
error_log( "cannot access to ClickUp task $task_id: " . $e->getMessage() );
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/task", [], $payload );
if( !isset( $task_details->id ) ) {
throw new Exception( "something bad happened saving a new ClickUp Task on List '$list_id' with this payload: " . json_encode( $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;
}
+ /**
+ * Try to create or update a ClickUp Task from a Phabricator Task PHID
+ */
+ public static function createOrUpdateFromPhabricatorTaskID( $id, $args = [] ) {
+ // query fresh Task data
+ $phab_task = PhabricatorAPI::getTaskByID( $id );
+ if( !$phab_task ) {
+ throw new Exception( "cannot get Phabricator Task by ID: {$id}" );
+ }
+ return self::createOrUpdateFromPhabricatorTaskData( $phab_task );
+ }
+
+ /**
+ * Try to create or update a ClickUp Task from a Phabricator Task PHID
+ */
+ public static function createOrUpdateFromPhabricatorTaskPHID( $phid, $args = [] ) {
+ // query fresh Task data
+ $phab_task = PhabricatorAPI::getTaskByPHID( $phid );
+ if( !$phab_task ) {
+ throw new Exception( "cannot get Phabricator Task by PHID: {$phid}" );
+ }
+ return self::createOrUpdateFromPhabricatorTaskData( $phab_task );
+ }
+
+ /**
+ * Try to create or update a ClickUp Task from a Phabricator Task's data
+ */
+ public static function createOrUpdateFromPhabricatorTaskData( $phab_task, $args = [] ) {
+
+ $args = array_replace( [
+ // after this threashold, a ClickUp task wil be not imported
+ // to disable this, set something really big
+ 'max-clickup-age-seconds' => 172800,
+ ], $args );
+
+ $cache = ClickUpPhabricatorCache::instance();
+
+ // array of stuff to be set in the ClickUp Task
+ $clickup_changes = [];
+
+ // ClickUp List in which we will save the Task
+ $clickup_list_id = null;
+
+ // resolved / open / etc.
+ $phab_task_id = $phab_task['id'];
+ $phab_task_phid = $phab_task['phid'];
+ $phab_task_status = $phab_task['fields']['status']['value'];
+ $phab_owner_phid = $phab_task['fields']['ownerPHID'] ?? null;
+
+ // check if the Phabricator Task is already known to be connected to ClickUp
+ $clickup_task = null;
+ $clickup_task_cache = $cache->getClickupTaskFromPhabricatorTaskID( $phab_task_id );
+ if( $clickup_task_cache ) {
+ $clickup_task = ClickUpAPI::queryTaskData( $clickup_task_cache->id );
+ }
+
+ // check if we already know the related ClickUp Task
+ if( $clickup_task ) {
+
+ // update an existing ClickUp Task
+
+ // check if Phabricator has a different Task status than ClickUp to be updated
+ $phab_task_is_closed = PhabricatorAPI::isStatusClosed( $phab_task_status );
+ $clickup_task_is_closed = ClickupAPI::isStatusClosed( $clickup_task->status->type );
+ if( $clickup_task->status->type !== $clickup_task_is_closed ) {
+ $clickup_changes['status'] = PhabricatorAPI::status2clickup( $phab_task_status );
+ }
+
+ // check if we have to add a Task assignee in ClickUp
+ if( $phab_owner_phid ) {
+ $clickup_assignee_id = $cache->findClickupUserIDbyPHID( $phab_owner_phid );
+ if( $clickup_assignee_id ) {
+ $clickup_changes['assignees.add'] = $clickup_assignee_id;
+ }
+ } else {
+ // TODO: somehow remove the removed assignee from ClickUp
+ }
+
+
+ // eventally update ClickUp with some changes
+ if( $clickup_changes ) {
+ ClickUpAPI::putTaskData( $clickup_task->id, [], $clickup_changes );
+ $clickup_changes = [];
+ }
+
+ } else {
+
+ // create a fresh ClickUp Task
+
+ // check if this is recent
+ $phab_task_data_created = $phab_task['fields']['dateCreated'];
+ $time_diff = time() - $phab_task_data_created;
+ if( $time_diff < $args['max-clickup-age-seconds'] ) {
+
+ // set ClickUp basic fields
+ $clickup_changes['name'] = $phab_task['fields']['name'];
+ $clickup_changes['status'] = PhabricatorAPI::status2clickup( $phab_task_status );
+
+ // import the description (adding the self Phabricator URL)
+ $clickup_changes['description'] = $phab_task['fields']['description']['raw'];
+ $clickup_changes['description'] .= "\n\n";
+ $clickup_changes['description'] .= PHABRICATOR_URL . "T{$phab_task_id}";
+ $clickup_changes['description'] = trim( $clickup_changes['description'] );
+
+ // try to assign this ClickUp Task to somebody
+ $clickup_assignee = $cache->findClickupUserbyPHID( $phab_owner_phid );
+ if( $clickup_assignee ) {
+ $clickup_changes['assignees'] = [ $clickup_assignee->id ];
+ } else {
+ error_log( "cannot determine ClickUp assignee from Phabricator Task T{$phab_task_id} that has owner {$phab_owner_phid} - please run ./cli/clickup-users.php" );
+ }
+
+ // check if this Phabricator Tag has some parents (and find a ClickUp parent)
+ $clickup_task_parent = null;
+ $phab_task_parent_phids = PhabricatorAPI::queryParentTaskPHIDsFromTaskPHID( $phab_task_phid );
+ $clickup_task_parents = $cache->getClickupTasksFromPhabricatorTaskPHIDs( $phab_task_parent_phids );
+
+ // to avoid {"err":"Cannot make subtasks of subtasks","ECODE":"ITEM_002"}
+ foreach( $clickup_task_parents as $i => $clickup_task_parent_candidate ) {
+ // replace these ClickUp tasks with their top parent
+ $clickup_task_parents[$i] = ClickUpAPI::findTopParentOfTaskID( $clickup_task_parent_candidate->id );
+ }
+
+ $clickup_task_parent = array_pop( $clickup_task_parents ); // just the first one asd
+
+ // we have a ClickUp Task parent! so we have a 100% sure List ID
+ if( $clickup_task_parent ) {
+
+ // let's save this ClickUp Task on the same List of the parent ClickUp Task
+ // and also let's connect the parent ClickUp Task to the current one
+ $clickup_list_id = $clickup_task_parent->list->id;
+ $clickup_changes['parent'] = $clickup_task_parent->id;
+
+ } else {
+
+ // try to find the most relevant ClickUp List from the Phabricator Task (from its Tags etc.)
+ $clickup_list = PhabricatorAPI::findClickupListFromTaskObject( $phab_task );
+ if( $clickup_list ) {
+ $clickup_list_id = $clickup_list->id;
+ } else {
+ error_log( "cannot determine ClickUp List for Phabricator Task T{$phab_task_id} - please run ./cli/clickup-folders.php" );
+ }
+ }
+
+ // if we know the ClickUp List ID, let's create this new ClickUp Task
+ if( $clickup_changes && $clickup_list_id ) {
+
+ // create the ClickUp task
+ $clickup_task = ClickUpAPI::createTaskOnList( $clickup_list_id, $clickup_changes );
+
+ $clickup_task_id = $clickup_task->id;
+
+ // after creation, reset changes, to avoid to save them again
+ $clickup_changes = [];
+
+ // associate the ClickUp task to Phabricator
+ $cache
+ ->lock()
+ ->registerClickupPhabricatorTaskID( $clickup_task_id, $phab_task_id )
+ ->registerClickupPhabricatorTaskPHID( $clickup_task_id, $phab_task_phid )
+ ->commit();
+
+ // edit the Phabricator Task comment to put a link to the ClickUp Task
+ $phab_new_description = $phab_task['fields']['description']['raw'];
+ $phab_new_description .= "\n\n";
+ $phab_new_description .= "> https://app.clickup.com/t/$clickup_task_id";
+ $phab_new_description = trim( $phab_new_description );
+
+ PhabricatorAPI::editTask( $phab_task_id, [
+ 'description' => $phab_new_description,
+ ] );
+ }
+
+ } else {
+ error_log( "Phabricator Task T{$phab_task_id} has not a related ClickUp Task and it's too old ($time_diff seconds)" );
+ }
+
+ }
+
+ return $clickup_task;
+ }
+
/**
* 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/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 = 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 );
case 'ACCESS_079':
throw new ClickupExceptionAccessDenied( $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", // this is nonsense since every time you visit a space it fires
"spaceDeleted",
"goalCreated",
"goalUpdated",
"goalDeleted",
"keyResultCreated",
"keyResultUpdated",
"keyResultDeleted",
];
}
}
diff --git a/public/webhook-phabricator/index.php b/public/webhook-phabricator/index.php
index 3aeb6e9..5d862c4 100644
--- a/public/webhook-phabricator/index.php
+++ b/public/webhook-phabricator/index.php
@@ -1,252 +1,136 @@
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 Phabricator webhooks
*
* Documentation:
* https://we.phorge.it/conduit/
*
* See also Herald rule.
*/
// default response code
http_response_code( 500 );
// load needed libraries
require realpath( __DIR__ ) . '/../autoload.php';
// parse HTTP payload
$response = file_get_contents('php://input');
// parse as an array since native Conduit also handles everything as an array
$response_data = json_decode( $response, JSON_OBJECT_AS_ARRAY );
// no signature no party
$signature_header = $_SERVER['HTTP_X_PHABRICATOR_WEBHOOK_SIGNATURE'] ?? null;
if( !$signature_header ) {
http_response_code( 400 );
echo "This is not a request from an official Phabricator webhook.\n";
exit;
}
// do the math
$signature_calculated = hash_hmac( 'sha256', $response, PHABRICATOR_WEBHOOK_HMAC_KEY );
if( $signature_calculated !== $signature_header ) {
http_response_code( 400 );
throw new Exception( "invalid signature, calculated $signature_calculated expected $signature_header" );
exit;
}
// no object no party
$object = $response_data['object'] ?? null;
if( !$object ) {
http_response_code( 400 );
throw new Exception( "received empty Phabricator object" );
}
$transactions = $response_data['transactions'] ?? [];
$object_type = $object['type'];
$object_phid = $object['phid'];
$cache = ClickUpPhabricatorCache::instance();
switch( $object_type ) {
case 'TASK':
// query fresh Task data
$phab_task = PhabricatorAPI::getTaskByPHID( $object_phid );
if( !$phab_task ) {
throw new Exception( "missing Task: {$object_phid}" );
}
- // array of stuff to be set in the ClickUp Task
- $clickup_changes = [];
-
- // ClickUp List in which we will save the Task
- $clickup_list_id = null;
-
- // resolved / open / etc.
$phab_task_id = $phab_task['id'];
$phab_task_phid = $phab_task['phid'];
$phab_task_status = $phab_task['fields']['status']['value'];
$phab_owner_phid = $phab_task['fields']['ownerPHID'] ?? null;
- // check if the Phabricator Task is already known to be connected to ClickUp
- $clickup_task = null;
- $clickup_task_cache = $cache->getClickupTaskFromPhabricatorTaskID( $phab_task_id );
- if( $clickup_task_cache ) {
- $clickup_task = ClickUpAPI::queryTaskData( $clickup_task_cache->id );
- }
-
- // check if we already know the related ClickUp Task
- if( $clickup_task ) {
-
- // update an existing ClickUp Task
-
- // check if Phabricator has a different Task status than ClickUp
- $phab_task_is_closed = PhabricatorAPI::isStatusClosed( $phab_task_status );
- $clickup_task_is_closed = ClickupAPI::isStatusClosed( $clickup_task->status->type );
-
- if( $clickup_task->status->type !== $clickup_task_is_closed ) {
-
- // update the Task status in ClickUp
- $clickup_changes['status'] = PhabricatorAPI::status2clickup( $phab_task_status );
- }
-
- // check if we have to add a Task assignee in ClickUp
- if( $phab_owner_phid ) {
- $clickup_assignee_id = $cache->findClickupUserIDbyPHID( $phab_owner_phid );
- if( $clickup_assignee_id ) {
- $clickup_changes['assignees.add'] = $clickup_assignee_id;
- }
- } else {
- // TODO: somehow remove the removed assignee from ClickUp
- }
-
- } else {
-
- // create a fresh ClickUp Task
-
- // check if this is recent
- $phab_task_data_created = $phab_task['fields']['dateCreated'];
- $time_diff = time() - $phab_task_data_created;
- if( $time_diff < 3000 ) {
-
- // set ClickUp basic fields
- $clickup_changes['name'] = $phab_task['fields']['name'];
- $clickup_changes['status'] = PhabricatorAPI::status2clickup( $phab_task_status );
-
- // import the description (adding the self Phabricator URL)
- $clickup_changes['description'] = $phab_task['fields']['description']['raw'];
- $clickup_changes['description'] .= "\n\n";
- $clickup_changes['description'] .= PHABRICATOR_URL . "T{$phab_task_id}";
- $clickup_changes['description'] = trim( $clickup_changes['description'] );
-
- // try to assign this ClickUp Task to somebody
- $clickup_assignee = $cache->findClickupUserbyPHID( $phab_owner_phid );
- if( $clickup_assignee ) {
- $clickup_changes['assignees'] = [ $clickup_assignee->id ];
- } else {
- error_log( "cannot determine ClickUp assignee from Phabricator Task T{$phab_task_id} that has owner {$phab_owner_phid} - please run ./cli/clickup-users.php" );
- }
-
- // check if this Phabricator Tag has some parents (and find a ClickUp parent)
- $clickup_task_parent = null;
- $phab_task_parent_phids = PhabricatorAPI::queryParentTaskPHIDsFromTaskPHID( $phab_task_phid );
- $clickup_task_parents = $cache->getClickupTasksFromPhabricatorTaskPHIDs( $phab_task_parent_phids );
-
- // to avoid {"err":"Cannot make subtasks of subtasks","ECODE":"ITEM_002"}
- foreach( $clickup_task_parents as $i => $clickup_task_parent_candidate ) {
- // replace these ClickUp tasks with their top parent
- $clickup_task_parents[$i] = ClickUpAPI::findTopParentOfTaskID( $clickup_task_parent_candidate->id );
- }
-
- $clickup_task_parent = array_pop( $clickup_task_parents ); // just the first one asd
-
- // we have a ClickUp Task parent! so we have a 100% sure List ID
- if( $clickup_task_parent ) {
-
- // let's save this ClickUp Task on the same List of the parent ClickUp Task
- // and also let's connect the parent ClickUp Task to the current one
- $clickup_list_id = $clickup_task_parent->list->id;
- $clickup_changes['parent'] = $clickup_task_parent->id;
-
- } else {
-
- // try to find the most relevant ClickUp List from the Phabricator Task (from its Tags etc.)
- $clickup_list = PhabricatorAPI::findClickupListFromTaskObject( $phab_task );
- if( $clickup_list ) {
- $clickup_list_id = $clickup_list->id;
- } else {
- error_log( "cannot determine ClickUp List for Phabricator Task T{$phab_task_id} - please run ./cli/clickup-folders.php" );
- }
-
- }
-
- // if we know the ClickUp List ID, let's create this new ClickUp Task
- if( $clickup_list_id && $clickup_changes ) {
-
- // create the ClickUp task
- $clickup_task = ClickUpAPI::createTaskOnList( $clickup_list_id, $clickup_changes );
-
- // after creation, reset changes, to avoid to run them again later
- $clickup_changes = [];
- }
-
- } else {
- error_log( "Phabricator Task T{$phab_task_id} has not a related ClickUp Task and it's too old ($time_diff seconds)" );
- }
- }
+ // create or update the ClickUp task from Phabricator Task data
+ $clickup_task = ClickUpAPI::createOrUpdateFromPhabricatorTaskData( $phab_task );
if( $transactions ) {
$transaction_results = PhabricatorAPI::searchObjectTransactionsFromTransactions( $phab_task_phid, $transactions );
foreach( $transaction_results as $transaction_result ) {
// found some new Phabricator comments
$phab_comments = $transaction_result['comments'] ?? [];
foreach( $phab_comments as $phab_comment ) {
// Phabricator comment content
$comment_content_raw = $phab_comment['content']['raw'];
// Phabricator author (NOTE: it MUST exists)
$comment_author = PhabricatorAPI::searchSingleUserByPHID( $phab_comment['authorPHID'] );
$comment_author_name = $comment_author['fields']['realName'];
// post the comment on ClickUp
if( $clickup_task ) {
// "From Phabricator: Mario Rossi:\n\nHello world!"
// IMPORTANT: keep the COMMENT_PREFIX_FROM_PHABRICATOR at startup since now
// the ClickUp webhook controls this token at startup.
$clickup_comment = sprintf(
"%s %s:\n\n%s",
COMMENT_PREFIX_FROM_PHABRICATOR,
$comment_author_name,
$comment_content_raw
);
ClickupAPI::addTaskComment( $clickup_task->id, $clickup_comment );
}
}
// check if we have missed something to be integrated
unset( $transaction_result['comments'] );
if( $transaction_result ) {
error_log( "received unsupported Phabricator transaction: " . json_encode( $transaction_result ) );
}
}
}
- // eventally update ClickUp with some changes
- if( $clickup_task && $clickup_changes ) {
- ClickUpAPI::putTaskData( $clickup_task->id, [], $clickup_changes );
- }
-
break;
case 'HWBH':
error_log( "received Phabricator Webhook (test?)" );
break;
default:
error_log( "unknown Phabricator webhook: {$response}" );
}
// success
http_response_code( 200 );