diff --git a/README.md b/README.md index 7c3e47c..9a385e4 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,57 @@ # ClickUp <-> Phabricator / Phorge bot Bot to help migration from ClickUp to Phabricator / Phorge (and vice-versa honestly). ## Available features * automatically import new ClickUp Tasks in Phabricator (with title, description) * automatically update the ClickUp Task to mention the Phabricator Task URL * automatically update the Phabricator Task to mention the ClickUp Task URL * automatically assign the right Phabricator Tag (associated to the corresponding ClickUp folder) +* automatically import new Phabricator Tasks in ClickUp (and keeping its ClickUp parent) * automatically import new ClickUp Task comments into their related Phabricator Task (and vice-versa) * automatically import new Phabricator Task owner when set from ClickUp * automatically update the Phabricator Task status when changed from ClickUp (and vice-versa) * automatically update the Phabricator Task title when changed from ClickUp * automatically create (and attach) a Phabricator parent Task if a ClickUp task has a parent task ## Dev preparation ``` sudo apt install php-cli composer ``` ## Dev installation ``` composer install ``` ## Configuration Copy the file `config-example.php` to `config.php`. Read carefully the example configuration file. ## License Copyright (C) 2023 Valerio Bozzolan, contributors This software was developed and tested and released in non-working hours so the copyright does NOT belong to the company. Any company can use, study, modify, share this software (as long as the terms are respected). Any company can do this, included ER Informatica, without Valerio's written consent. You're welcome. 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 . diff --git a/public/webhook-phabricator/index.php b/public/webhook-phabricator/index.php index 69fdf7b..2770f1c 100644 --- a/public/webhook-phabricator/index.php +++ b/public/webhook-phabricator/index.php @@ -1,208 +1,239 @@ 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 ); } - // no related ClickUp Task? - if( !$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 < 600 ) { - - // try to find the most relevant ClickUp List - $clickup_list = PhabricatorAPI::findClickupListFromTaskObject( $phab_task ); - if( $clickup_list ) { - - error_log( "creating ClickUp Task from Phabricator Task T{$phab_task_id} on ClickUp List '{$clickup_list->name}' ID {$clickup_list->id}" ); - - // try to assign this ClickUp Task to somebody - $clickup_assignees = []; - $clickup_assignee = $cache->findClickupUserbyPHID( $phab_owner_phid ); - if( $clickup_assignee ) { - $clickup_assignees[] = $clickup_assignee; - } 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" ); - } - - error_log( "to be implemented: find ClickUp parent Task from Conduit API edge.search with edge type = task.parent https://sviluppo.erinformatica.it/conduit/method/edge.search/" ); - - // create in ClickUp as well -// $clickup_task = ClickUpAPI::createTaskOnList( $clickup_list->id, [ -// 'name' => $phab_task['fields']['name'], -// 'description' => $phab_task['fields']['description']['raw'], -// 'assignees' => $clickup_assignees, -// 'status' => PhabricatorAPI::status2clickup( $phab_task_status ), -// 'parent' => '', -// ] ); - - } else { - error_log( "cannot determine ClickUp List for Phabricator Task T{$phab_task_id} - please run ./cli/clickup-folders.php" ); - } - - } else { - error_log( "Phabricator Task T{$phab_task_id} has not a related ClickUp Task and it's too old ($time_diff seconds)" ); - } - } - + // 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 ); + $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['description'] = $phab_task['fields']['description']['raw']; + $clickup_changes['status'] = PhabricatorAPI::status2clickup( $phab_task_status ); + + // 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) + $phab_task_parent_phids = PhabricatorAPI::queryParentTaskPHIDsFromTaskPHID( $phab_task_phid ); + $clickup_task_parents = $cache->getClickupTasksFromPhabricatorTaskPHIDs( $phab_task_parent_phids ); + $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)" ); + } } 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 );