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 );