diff --git a/include/class-ClickUpAPI.php b/include/class-ClickUpAPI.php
index 53ff017..e11a004 100644
--- a/include/class-ClickUpAPI.php
+++ b/include/class-ClickUpAPI.php
@@ -1,289 +1,308 @@
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;
}
+ /**
+ * 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;
}
$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/include/class-PhabricatorAPI.php b/include/class-PhabricatorAPI.php
index 70fd623..a1e762e 100644
--- a/include/class-PhabricatorAPI.php
+++ b/include/class-PhabricatorAPI.php
@@ -1,198 +1,210 @@
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 {
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,
] );
}
/**
- * Convert a Phabricator status to a Clickup status
+ * Check if a Phabricator status is equivalent to "closed"
+ *
+ * @return self
*/
- public static function status2clickup( $task_status ) {
+ 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',
];
- if( in_array( $task_status, $closed_statuses, true ) ) {
+ 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 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::searchSingleResult( 'maniphest.search', $query );
}
public static function searchSingleUser( $query ) {
return self::searchSingleResult( 'user.search', $query );
}
public static function searchSingleResult( $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;
}
}
diff --git a/public/webhook-clickup/index.php b/public/webhook-clickup/index.php
index 65898a2..25036cf 100644
--- a/public/webhook-clickup/index.php
+++ b/public/webhook-clickup/index.php
@@ -1,199 +1,205 @@
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;
$webhook_id = $_SERVER['HTTP_X_SIGNATURE'] ?? 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;
}
// check incoming request
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";
http_response_code( 400 );
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";
}
$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':
// 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 '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 );
- if( $clickup_task_status_for_phab === 'resolved' || $clickup_task_status_for_phab === 'open' ) {
- // fetch fresh Phabricator Task data
- $phab_task = PhabricatorAPI::getTask( $phab_task_id );
- $phab_task_status = $phab_task['fields']['status']['value'];
+ // 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_for_phab - Phab Task $phab_task_id status: $phab_task_status - updating" );
+ 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( "do nothing for status: $clickup_task_status (Phab: $clickup_task_status_for_phab)" );
+ 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 );
diff --git a/public/webhook-phabricator/index.php b/public/webhook-phabricator/index.php
index d026b7d..2005873 100644
--- a/public/webhook-phabricator/index.php
+++ b/public/webhook-phabricator/index.php
@@ -1,169 +1,172 @@
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 );
// check incoming request
if( PHABRICATOR_VALIDATE_WEBHOOK ) {
// 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 );
error_log( "invalid signature, calculated $signature_calculated" );
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}" );
}
$clickup_changes = [];
// resolved / open / etc.
$phab_task_status = $phab_task['fields']['status']['value'];
$phab_task_id = $phab_task['id'];
$phab_task_phid = $phab_task['phid'];
// 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 );
}
// no related ClickUp Task?
if( !$clickup_task ) {
// TODO: find Task projects
$phab_projects = [];
// TODO: find the related ClickUp folder
$clickup_folder = null;
error_log( "Phabricator Task T{$phab_task_id} has not a related ClickUp Task" );
}
if( $clickup_task ) {
+
// check if Phabricator has a different Task status than ClickUp
- $clickup_task_status_for_phab = ClickUpAPI::taskStatusTypeToPhabricator( $clickup_task->status );
- if( $clickup_task_status_for_phab !== $phab_task_status ) {
+ $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 );
}
}
$phab_fields = $phab_task['fields'];
$phab_attachments = $phab_task['attachments'];
$phab_project_phids = $phab_attachments['projects']['projectPHIDs'];
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 );
}
}
}
}
// 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;
}
// success
http_response_code( 200 );