diff --git a/config-example.php b/config-example.php index de94423..dd28dbf 100644 --- a/config-example.php +++ b/config-example.php @@ -1,84 +1,92 @@ 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 . // ClickUp Team ID - you can find it from your home URL // Example: "123" if your home URL is "https://app.clickup.com/123/v/l/li/456456456456" define( 'CLICKUP_TEAM_ID', 123 ); +// ClickUp dedicated user ID +// +// fill this configuration with the numeric ClickUp user ID +// You can read this value from the cache/data.json +// this configuration is ONLY used to detect changes made by our bot +// and that so should be skipped to avoid loops. +define( 'CLICKUP_DEDICATED_USER_ID', 1234567890 ); + // ClickUp API token // // https://clickup.com/api/developer-portal/authentication#personal-token define( 'CLICKUP_API_TOKEN', 'pk_123_ASD123' ); // ClickUp Webhook ID // // You obtain this after registering your webhook using this script: // ./cli/clickup-webhook-create.php // You can always see this value using the tool: // ./cli/clickup-webhook-list.php // This is only used to be able to quickly delete your webhook using the related script: // ./cli/clickup-webhook-delete.php define( 'CLICKUP_REGISTERED_WEBHOOK_ID', '123-123-123-123-123' ); // ClickUp HMAC secret key // // The ClickUp HMAC "secret" key provided from the ClickUp webhook, after you create it. // You can always see this value using the tool: // ./cli/clickup-webhook-list.php // Documentation: // https://clickup.com/api/developer-portal/webhooksignature/ define( 'CLICKUP_WEBHOOK_HMAC_KEY', '123ASD123ASD123ASD123ASD123ASD123ASD123ASD123ASD123ASD' ); // Phabricator Conduit API token // You can create this token by creating a simple User of type = bot // in your Phabricator and then going to this page (from that user): // https://yourphabricator/conduit/token/ define( 'PHABRICATOR_CONDUIT_API_TOKEN', 'api-123asd123asd123asd123asd123' ); // path to the Arcanist library of your Phabricator installation (to call APIs) define( 'PHABRICATOR_ARCANIST_PATH', '/var/www/phabricator/arcanist/support/init/init-script.php' ); // Phabricator home URL (to call API) // MUST end with a slash define( 'PHABRICATOR_URL', 'https://yourphabricator/' ); // URL exposing the "webhook-phabricator" directory from this repository // NOTE: this MUST be a valid and public URL so ClickUp can use that as webhook // NOTE: the checkorigin token is just a random token you should invent define( 'PHABRICATOR_WEBHOOK_PUBLIC_ENDPOINT', 'https://example.com/clickup-phabricator-phorge-bot/webhook-phabricator/' ); // URL exposing the "webhook-clickup" directory from this repository // NOTE: this MUST be a valid and public URL so ClickUp can use that as webhook // NOTE: the checkorigin token is just a random token you should invent define( 'CLICKUP_WEBHOOK_PUBLIC_ENDPOINT', 'https://example.com/clickup-phabricator-phorge-bot/webhook-clickup/?checkorigintoken=abcdefghilmpopqrstuvz123' ); // the crypto Phabricator Webhook HMAC key obtained from the Herald > Webhooks page define( 'PHABRICATOR_WEBHOOK_HMAC_KEY', 'abcdefghilmpopqrstuvz123' ); // ClickUp cache (connecting its stuff to Phabricator, avoid creating duplicate tasks, etc.) define( 'CLICKUP_CACHE_JSON_FILE', __DIR__ . '/data/cache.json' ); // ClickUp cache pretty print define( 'CLICKUP_CACHE_JSON_PRETTY', true ); // comment prefix from Phabricator to ClickUp // this is also used to identify comments posted FROM Phabricator TO ClickUp from our bot itself define( 'COMMENT_PREFIX_FROM_PHABRICATOR', "From Phabricator:" ); // homepage of the project // just to know where you can send bugs define( 'REPOSITORY_URL', 'https://gitpull.it/source/clickup-phabricator-bot/' ); diff --git a/public/webhook-clickup/index.php b/public/webhook-clickup/index.php index 7ed64ef..3fd769a 100644 --- a/public/webhook-clickup/index.php +++ b/public/webhook-clickup/index.php @@ -1,223 +1,227 @@ 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; // 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 ) ->registerClickupPhabricatorTaskPHID( $clickup_task_id, $phab_task_phid ) ->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 ) { + // verify that the user who posted the comment is not ourself + if( $clickup_comment_author_id == CLICKUP_DEDICATED_USER_ID ) { - error_log( "skip ClickUp comment that was posted by the Phabricator bot" ); + // avoid to post on Phabricator, comments that are coming from our Phabricator bot + if( strpos( $clickup_task_description, COMMENT_PREFIX_FROM_PHABRICATOR ) === 0 ) { - } else { + 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, - ] ); + // 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( "skip ClickUp Task $clickup_task_id with status $clickup_task_status (Phab was $phab_task_status)" ); } } break; default: echo "Method not supported.\n"; error_log( "unsupported ClickUp webhoook: $event - $response" ); } // success http_response_code( 200 );