diff --git a/cli/clickup-folders.php b/cli/clickup-folders.php
index 8c58093..c6d2096 100755
--- a/cli/clickup-folders.php
+++ b/cli/clickup-folders.php
@@ -1,109 +1,148 @@
#!/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';
// ask data
+$import = true;
$populate = true;
$cache = ClickUpPhabricatorCache::instance();
// import all ClickUp Folders from all Spaces
-$clickup_spaces = ClickUpAPI::getSpaces();
-foreach( $clickup_spaces as $space ) {
+if( $import ) {
+ echo "Start import\n";
- // $space->id
- // $space->name
+ $clickup_spaces = ClickUpAPI::getSpaces();
+ foreach( $clickup_spaces as $space ) {
- // save each folder in the cache
- $cache->lock();
- $folders = ClickUpAPI::getSpaceFolders( $space->id );
- foreach( $folders as $folder ) {
-
- // $folder->id
- // $folder->name
-
- $cache->importClickupFolder( $folder );
+ // also query space folders (it will import them)
+ echo "Importing folders of space {$space->name}\n";
+ ClickUpAPI::getSpaceFolders( $space->id );
+ sleep( 1 );
}
- $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 && $populate ) {
$phab_tag_id = readline( "Specify the Phabricator Tag name for {$folder->name} (example: pippo_pluto): " );
}
if( $phab_tag_id && !$phab_tag_phid ) {
// find fresh project from its slug
- $phab_project = PhabricatorAPI::querySingle( 'project.search', [
- 'constraints' => [
- 'slugs' => [ $phab_tag_id, ]
- ],
- ] );
-
+ $phab_project = PhabricatorAPI::querySingleProjectBySlug( $phab_tag_id );
if( $phab_project ) {
// associate the ClickUp folder to the Phabricator Tag PHID
$cache
->lock()
->associateClickupFolderIDPhabricatorID( $folder->id, $phab_project['fields']['slug'] )
->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->lock();
- foreach( $phab_columns['data'] as $phab_column ) {
- $cache->associateClickupFolderIDPhabricatorColumnPHID( $folder->id, $phab_column['phid'] );
+ ->commit();
+ }
+ }
+}
+
+// loop cached folders
+foreach( $cache->getClickUpLists() as $list ) {
+
+ $phab_project = null;
+ $list_folder_id = $list->folderID;
+ $list_phab_tag_id = $list->phabData->id;
+ $list_phab_tag_phid = $list->phabData->phid;
+ $list_phab_tag_col_phid = $list->phabData->columnPhid;
+
+ $list_folder = $cache->getClickupFolderByID( $list_folder_id );
+
+ if( !$list_phab_tag_id && !$list_phab_tag_phid ) {
+
+ // try to automatically search a Phabricator Tag
+ $phab_project = PhabricatorAPI::guessPhabricatorTagFromHumanName( $list->name );
+ if( $phab_project ) {
+
+ // allow to undo the proposal
+ if( 'n' === readline( "Found Phabricator Tag {$phab_project['fields']['name']} for ClickUp Folder {$list_folder->name} > {$list->name}. Confirm with ENTER or press 'n' to undo." ) ) {
+ $phab_project = null;
}
- $cache->save();
- */
}
+
+ // not found? ask manually (and probably it's a column)
+ if( !$phab_project ) {
+
+ // ask Tag but allow to set a specific column
+ $list_phab_tag_id = readline( "Type the Phabricator Tag slug for ClickUp folder {$list_folder->name} list {$list->name} (example: pippo_pluto) or ENTER to skip (or the PHID-PCOL ID for a column): " );
+ if( strpos( $list_phab_tag_id, 'PHID-PCOL' ) === 0 ) {
+
+ $list_phab_tag_col_phid = $list_phab_tag_id;
+ $list_phab_tag_id = null;
+
+ $phab_column = PhabricatorAPI::querySingleProjectColumnByPHID( $list_phab_tag_col_phid );
+ $list_phab_tag_id = $phab_column['fields']['project']['id'];
+ $list_phab_tag_phid = $phab_column['fields']['project']['phid'];
+
+ // associate this column to the ClickUp list
+ $cache
+ ->lock()
+ ->associateClickupListIDPhabricatorColumnPHID( $list->id, $list_phab_tag_col_phid )
+ ->associateClickupListIDPhabricatorPHID( $list->id, $list_phab_tag_phid )
+ ->associateClickupListIDPhabricatorID( $list->id, $list_phab_tag_id )
+ ->commit();
+ }
+ }
+ }
+
+ // expand our knowledge
+ if( !$phab_project && $list_phab_tag_id && !$list_phab_tag_phid ) {
+ $phab_project = PhabricatorAPI::querySingleProjectBySlug( $list_phab_tag_id );
}
+
+ // expand our knowledge
+ if( $phab_project ) {
+ $cache
+ ->lock()
+ ->associateClickupListIDPhabricatorID( $list->id, $phab_project['fields']['slug'] )
+ ->associateClickupListIDPhabricatorPHID( $list->id, $phab_project['phid'] )
+ ->commit();
+ }
+
}
// loop cached folders
foreach( $cache->getClickUpFolders() as $folder ) {
$phab_tag_id = $cache->getPhabricatorTagIDFromClickupFolderID( $folder->id );
$phab_tag_phid = $cache->getPhabricatorTagPHIDFromClickupFolderID( $folder->id );
$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/include/class-PhabricatorAPI.php b/include/class-PhabricatorAPI.php
index a015357..c4327c7 100644
--- a/include/class-PhabricatorAPI.php
+++ b/include/class-PhabricatorAPI.php
@@ -1,273 +1,289 @@
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 {
/**
* Query a single Phabricator Project by its slug
*
* If this is not possible, it throws an exception.
*
- * @param string $slug Example 'foo_bar'
+ * @param $slug string Example 'foo_bar'
* @return mixed
*/
public static function querySingleProjectBySlug( $slug ) {
return PhabricatorAPI::querySingle( 'project.search', [
'constraints' => [
'slugs' => [ $slug ],
],
] );
}
+ /**
+ * Query a single Phabricator Project's Column by its PHID
+ *
+ * If this is not possible, it throws an exception.
+ *
+ * @param $phid string
+ * @return mixed
+ */
+ public static function querySingleProjectColumnByPHID( $phid ) {
+ return PhabricatorAPI::querySingle( 'project.column.search', [
+ 'constraints' => [
+ 'phids' => [ $phid ],
+ ],
+ ] );
+ }
+
/**
* Try to guess a single Phabricator Project by its human name
*
* If this is not possible, NULL is returned.
*
* @param string $slug Example 'Foo Bar'
* @return mixed
*/
public static function guessPhabricatorTagFromHumanName( $name ) {
$name = str_replace( ' ', '_', $name );
$name = strtolower( $name );
$project = null;
try {
$project = self::querySingleProjectBySlug( $name );
} catch( Exception $e ) {
// do nothing
}
return $project;
}
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,
] );
}
/**
* Check if a Phabricator status is equivalent to "closed"
*
* @return self
*/
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',
];
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 getUserByPHID( $phid ) {
$query = [
'constraints' => [
'phids' => [ $phid ],
],
];
return self::querySingle( 'user.search', $query );
}
public static function getUserByUsername( $username ) {
$query = [
'constraints' => [
'usernames' => [ $username ],
],
];
return self::querySingle( 'user.search', $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::querySingle( 'maniphest.search', $query );
}
public static function searchSingleUser( $query ) {
return self::querySingle( 'user.search', $query );
}
/**
* Return a single element from Phabricator or throw an exception
*
* @param $method string
* @param $query array
* @return mixed
*/
public static function querySingle( $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;
}
}