diff --git a/cli/clickup-folders.php b/cli/clickup-folders.php
index 0c58070..cbab27a 100755
--- a/cli/clickup-folders.php
+++ b/cli/clickup-folders.php
@@ -1,90 +1,90 @@
#!/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 .
/**
* Get ClickUp folders
*/
// autoload libraries
require __DIR__ . '/../autoload.php';
$cache = ClickUpPhabricatorCache::instance();
// import all ClickUp Folders from all Spaces
$clickup_spaces = ClickUpAPI::getSpaces();
foreach( $clickup_spaces as $space ) {
// $space->id
// $space->name
// save each folder in the cache
- $cache->newlock();
+ $cache->lock();
$folders = ClickUpAPI::getSpaceFolders( $space->id );
foreach( $folders as $folder ) {
// $folder->id
// $folder->name
$cache->importClickupFolder( $folder );
}
$cache->save();
}
// loop cached folders
foreach( $cache->getClickUpFolders() as $folder ) {
$phab_tag_id = $cache->getPhabricatorTagIDFromClickupFolderID( $folder->id );
$phab_tag_phid = $cache->getPhabricatorTagPHIDFromClickupFolderID( $folder->id );
if( $phab_tag_id ) {
// find fresh project from its slug
$phab_project = PhabricatorAPI::querySingle( 'project.search', [
'constraints' => [
'slugs' => [ $phab_tag_id, ]
],
] );
// associate the ClickUp folder to the Phabricator Tag PHID
$cache
- ->newlock()
+ ->lock()
->associateClickupFolderIDPhabricatorPHID( $folder->id, $phab_project['phid'] )
->save();
/*
// find columns from this project
$phab_columns = PhabricatorAPI::query( 'project.column.search', [
'constraints' => [
'projects' => [ $phab_project['phid'] ],
],
] );
- $cache->newlock();
+ $cache->lock();
foreach( $phab_columns['data'] as $phab_column ) {
$cache->associateClickupFolderIDPhabricatorColumnPHID( $folder->id, $phab_column['phid'] );
}
$cache->save();
*/
}
$phab_tag_id_msg = $phab_tag_id
? "HAS Phab ID"
: "MISSING Phab ID";
// print something nice to see
echo $folder->id . "\t|" . $phab_tag_id_msg . "\t|" . $folder->name . "\n";
}
diff --git a/cli/clickup-task.php b/cli/clickup-task.php
index dd1a053..3d5310c 100755
--- a/cli/clickup-task.php
+++ b/cli/clickup-task.php
@@ -1,53 +1,56 @@
#!/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 .
/**
* Get ClickUp Task info
*
* If you specify also a Phabricator Task ID, it connects both.
*/
// autoload libraries
require __DIR__ . '/../autoload.php';
$clickup_task_id = $argv[1] ?? null;
$phab_task_id = $argv[2] ?? null;
if( !$clickup_task_id ) {
echo "RTFM\n";
exit;
}
$clickup_task_details = ClickUpAPI::queryTaskData( $clickup_task_id );
print_r( $clickup_task_details );
echo "\n";
if( $phab_task_id ) {
$phab_task_details = PhabricatorAPI::getTask( $phab_task_id );
if( $phab_task_details ) {
$phab_task_id = $phab_task_details['id'];
if( $phab_task_details['type'] === 'TASK' ) {
- ClickUpPhabricatorCache::instance()->newlock()->registerClickupPhabricatorTaskID( $clickup_task_id, $phab_task_id )->save();
+ ClickUpPhabricatorCache::instance()
+ ->lock()
+ ->registerClickupPhabricatorTaskID( $clickup_task_id, $phab_task_id )
+ ->save();
}
}
print_r( $phab_task_details );
echo "\n";
}
diff --git a/include/class-ClickUpAPI.php b/include/class-ClickUpAPI.php
index 739d483..6ac0e6e 100644
--- a/include/class-ClickUpAPI.php
+++ b/include/class-ClickUpAPI.php
@@ -1,317 +1,317 @@
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()
+ ->lock()
->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()
+ ->lock()
->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 ) ) {
// 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-ClickUpPhabricatorCache.php b/include/class-ClickUpPhabricatorCache.php
index a670a01..95b0fb8 100644
--- a/include/class-ClickUpPhabricatorCache.php
+++ b/include/class-ClickUpPhabricatorCache.php
@@ -1,497 +1,492 @@
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 .
/**
* A simple but robust cache system to store stuff about ClickUp/Phabricator
*
* Workflow:
* lock()->...->save()->close()
*/
class ClickUpPhabricatorCache {
/**
* Cache file pathname
*
* @param string
*/
private $file;
/**
* File pointer
*
* NULL: virgin
* false: error
* other: valid file pointer
*
* @var mixed
*/
private $fp;
/**
* Check if we are under a file lock
*/
private $locked;
/**
* Cache content
*
* @var mixed
*/
private $cache;
/**
* This singleton instance
*
* @var self
*/
private static $_instance;
/**
* Get the singleton instance
*
* @return self
*/
public static function instance() {
// instantiate only once
if( !self::$_instance ) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* Constructor
*
* Please DON'T call this directly.
* Use the instance() static method instead.
*
* @param $file string
*/
public function __construct( $file = null ) {
// take default from global configuration
if( !$file ) {
$file = CLICKUP_CACHE_JSON_FILE;
}
$this->file = $file;
}
/**
* Destructor
*
* It automatically closes any file pointer.
* NOTE: it does not write anything.
*/
function __destruct() {
$this->close();
}
- /**
- * Acquire a new lock to read and then write
- *
- * @return self
- */
- public function newlock() {
- return $this->close()->lock();
- }
-
/**
* Assure a lock on the file
*
* @return self
*/
public function lock() {
// no need to lock twice
if( !$this->locked ) {
$this->forceLock();
}
// make chainable
return $this;
}
/**
* Write the cache to filesystem in a secure way
*
- * NOTE: this does NOT close and does not free any lock.
- * You may need to call close() after this.
+ * NOTE: this closes and frees any lock.
* @return self
*/
public function save() {
return $this->lock()->write()->close();
}
/**
* Close the file, freeing any related file lock
*
* @return self
*/
public function close() {
// no need to close twice
if( $this->fp ) {
fclose( $this->fp );
}
// mark resources as free to be re-used again
$this->fp = null;
$this->cache = null;
$this->locked = null;
// make chainable
return $this;
}
public function getAllClickupTasks() {
$cache = $this->getCache();
if( !isset( $cache->clickupTasks ) ) {
$cache->clickupTasks = new StdClass();
}
return $cache->clickupTasks;
}
public function getClickupTask( $click_id ) {
// get all indexed tasks
$tasks = $this->getAllClickupTasks();
// get or create one Task
$tasks->{ $click_id } = $tasks->{ $click_id } ?? new StdClass();
$task = $tasks->{ $click_id };
// get or create task name
// $task->name = $task->name ?? null;
// get or create its folder
$task->folder = $task->folder ?? new StdClass();
// get or create one Tasks's Phabricator data
$task->phabData = $task->phabData ?? new StdClass();
$phab_data = $task->phabData;
// get or create one Tasks's Phabricator data ID
$phab_data->id = $phab_data->id ?? "";
return $task;
}
public function removeClickupTask( $click_id ) {
$tasks = $this->getAllClickupTasks();
unset( $tasks->{ $click_id } );
}
/**
* Import a ClickUp Task having its ID and its data
* retrieved from its API
*
* @param $click_id string
* @param $data array
* @return self
*/
public function importClickupTaskData( $click_id, $data ) {
$task = $this->getClickUpTask( $click_id );
// import name or leave as-is
// $task->name = $data->name ?? null;
// import useful fields
// NOTE: folder always exists here
$task->folder->id = $data->folder->id ?? null;
// $task->folder->name = $data->folder->name ?? null;
// try to read Phabricator data (so the cache is created at least once)
$this->getClickUpTaskPhabricatorData( $click_id );
// let's also import the related folder info
$this->importClickUpFolder( $data->folder );
// make chainable
return $this;
}
public function getClickupTaskPhabricatorData( $click_id ) {
// NOTE: phabData always exists
return $this->getClickUpTask( $click_id )->phabData;
}
public function getClickupTaskPhabricatorID( $click_id ) {
// NOTE: ID always exists
return $this->getClickupTaskPhabricatorData( $click_id )->id;
}
public function getClickupTaskFromPhabricatorTaskID( $phab_task_id ) {
$phab_task_id = PhabricatorAPI::sanitize_task_id( $phab_task_id );
// just search
$clickup_tasks = $this->getAllClickupTasks();
foreach( $clickup_tasks as $clickup_id => $clickup_task ) {
if( $clickup_task->phabData->id === $phab_task_id ) {
// expose the ClickUp task ID obtained from the array key
$clickup_task->id = $clickup_id;
return $clickup_task;
}
}
return false;
}
public function registerClickupPhabricatorTaskID( $click_id, $phab_id ) {
$phab_data = $this->getClickupTaskPhabricatorData( $click_id );
$phab_data->id = $phab_id;
// make chainable
return $this;
}
public function associateClickupFolderIDPhabricatorPHID( $folder_id, $phab_phid ) {
$this->getClickupFolderByID( $folder_id )->phabData->phid = $phab_phid;
return $this;
}
public function getClickUpFolders() {
$cache = $this->getCache();
$cache->clickupFolders = $cache->clickupFolders ?? new StdClass();
return $cache->clickupFolders;
}
public function getClickupFolderByID( $id ) {
$folders = $this->getClickupFolders();
$folders->{ $id } = $folders->{ $id } ?? new StdClass();
$folder = $folders->{ $id };
$folder->phabData = $folder->phabData ?? new StdClass();
$folder->phabData->id = $folder->phabData->id ?? "";
$folder->phabData->phid = $folder->phabData->phid ?? "";
return $folder;
}
public function getPhabricatorTagPHIDFromClickupFolderID( $click_id ) {
return $this->getClickupFolderByID( $click_id )->phabData->phid;
}
public function getPhabricatorTagIDFromClickupFolderID( $click_id ) {
return $this->getClickupFolderByID( $click_id )->phabData->id;
}
public function getPhabricatorTagFromClickupFolderID( $id ) {
$folder = $this->getClickupFolderByID( $id );
return $folder->phabData->id;
}
public function importClickupFolder( $data ) {
$id = $data->id;
$folder = $this->getClickupFolderByID( $id );
$folder->id = $data->id;
$folder->name = $data->name;
// make chainable
return $this;
}
/**
* Get the cache content
*
* @return mixed
*/
public function getCache() {
return $this->read()->cache;
}
/**
* Open the file
*
* @return self
*/
public function open() {
// just assure that there is a file pointer
$this->getFilePointer();
// make chainable
return $this;
}
/**
* Get the file pointer to the cache file
*
* This does NOT lock anything. You have to call lock() if you need.
*
* @return self
*/
private function getFilePointer() {
// avoid to open twice
if( $this->virginFilePointer() ) {
// open the file if it already exists
// create the file if it does not exist
$this->fp = @fopen( $this->file, 'c+' );
if( !$this->fp ) {
$this->throwFileException( "cannot open file" );
}
}
return $this->fp;
}
/**
* Check if the file pointer was never allocated
*
* @return bool
*/
private function virginFilePointer() {
return $this->fp === null;
}
/**
* Read and parse
*
* This method automatically open the file.
* This method is safe to be called multiple times.
*
* @return self
*/
private function read() {
// read just at startup or after any close
if( !$this->cache ) {
$this->forceRead();
}
// make chainable
return $this;
}
/**
* Force reading the content of the file
*/
private function forceRead() {
$fp = $this->getFilePointer();
// read content from opened file
$content = '';
while( !feof( $fp ) ) {
// read in chunks
$part = fread( $fp, 8192 );
// no chunk no party
if( $part === false ) {
$this->throwFileException( "cannot read file" );
}
$content .= $part;
}
// assume at least a valid empty JSON
if( !$content ) {
$content = '{}';
}
// try to parse content
$data = @json_decode( $content );
if( $data === false ) {
$this->throwFileException( "cannot parse JSON" );
}
// remember parser data
$this->cache = $data;
}
/**
* Force a lock on the file
*
* @param bool
*/
public function forceLock() {
$fp = $this->getFilePointer();
$locked = @flock( $fp, LOCK_EX );
if( !$locked ) {
$this->throwFileException( "cannot acquire lock" );
}
$this->locked = true;
+
+ // after a lock, mark the cache as to-be-read-again
+ // so we are sure that data is read after the lock
+ // and read+save is consistent
+ $this->cache = null;
}
/**
* Write the cache on the filesystem
*
* @return self
*/
private function write() {
$flags = 0;
if( CLICKUP_CACHE_JSON_PRETTY ) {
$flags = JSON_PRETTY_PRINT;
}
$fp = $this->getFilePointer();
// encode the cache in JSON
$raw = @json_encode( $this->getCache(), $flags );
if( $raw === false ) {
throw new Exception( "cannot JSON encode" );
}
// clear the file
$ok = @ftruncate( $fp, 0 );
if( !$ok ) {
$this->throwFileException( "cannot truncate" );
}
// turn back to the start of the file
$ok = @rewind( $fp );
if( !$ok ) {
$this->throwFileException( "cannot rewind" );
}
// try to write
$ok = @fwrite( $fp, $raw );
if( !$ok ) {
$this->throwFileException( "cannot write" );
}
// make chainable
return $this;
}
/**
* Create an exception with some file info
*
* @param $msg
*/
private function throwFileException( $msg ) {
throw new Exception( sprintf(
"error: %s (file: %s, current user: %s)",
$msg,
$this->file,
getmyuid()
) );
}
}
diff --git a/public/webhook-clickup/index.php b/public/webhook-clickup/index.php
index 25036cf..591c987 100644
--- a/public/webhook-clickup/index.php
+++ b/public/webhook-clickup/index.php
@@ -1,205 +1,208 @@
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();
+ $cache
+ ->lock()
+ ->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 );
// 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 );