diff --git a/cli/clickup-users.php b/cli/clickup-users.php
new file mode 100755
index 0000000..3183847
--- /dev/null
+++ b/cli/clickup-users.php
@@ -0,0 +1,47 @@
+#!/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 users
+ */
+
+// autoload libraries
+require __DIR__ . '/../autoload.php';
+
+$cache = ClickUpPhabricatorCache::instance();
+
+$max = 5;
+$i = 0;
+
+$clickup_users = $cache->getClickUpUsers();
+foreach( $clickup_users as $clickup_user ) {
+}
+
+$clickup_tasks = $cache->getAllClickupTasks();
+foreach( $clickup_tasks as $clickup_task_id => $clickup_task ) {
+
+ $clickup_task_details = ClickUpAPI::queryTaskData( $clickup_task_id );
+ if( $clickup_task_details ) {
+ print_r( $clickup_task_details->creator );
+ print_r( $clickup_task_details->assignees );
+ print_r( $clickup_task_details->watchers );
+ exit;
+ }
+
+ sleep( 1 );
+}
diff --git a/include/class-ClickUpPhabricatorCache.php b/include/class-ClickUpPhabricatorCache.php
index d9f02fb..5b295eb 100644
--- a/include/class-ClickUpPhabricatorCache.php
+++ b/include/class-ClickUpPhabricatorCache.php
@@ -1,546 +1,554 @@
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();
}
/**
* 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 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;
}
+ /**
+ * Remove a ClickUp Task from the cache
+ *
+ * @return self
+ */
public function removeClickupTask( $click_id ) {
$tasks = $this->getAllClickupTasks();
unset( $tasks->{ $click_id } );
+ return $this;
}
/**
* 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 );
$this->importClickUpUser( $data->creator );
$this->importClickupUsers( $data->assignees );
$this->importClickupUsers( $data->watchers );
// 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 getClickUpUsers() {
$cache = $this->getCache();
$cache->clickupUsers = $cache->clickupUsers ?? new StdClass();
return $cache->clickupUsers;
}
public function importClickUpUser( $user_data ) {
$this->getClickUpUserByID( $user_data->id, $user_data );
return $this;
}
public function importClickUpUsers( $users_data ) {
foreach( $users_data as $user_data ) {
$this->importClickUpUser( $user_data );
}
return $this;
}
public function getClickUpUserByID( $id, $existing_data = null ) {
$users = $this->getClickUpUsers();
// get or create
$user = $users->{ $id } = $users->{ $id } ?? new stdclass();
// eventually create if not found
if( $existing_data ) {
$user->username = $existing_data->username;
}
- // get or init
- $user->phabData = $user->phabData ?? new stdclass();
- $user->phabData->id = $user->phabData->id ?? "";
- $user->phabData->phid = $user->phabData->phid ?? "";
+ // get or init Phabricator data
+ $phabData = $user->phabData = $user->phabData ?? new stdclass();
+
+ // assure initialization of useful User fields
+ $phabData->id = $phabData->id ?? "";
+ $phabData->phid = $phabData->phid ?? "";
return $user;
}
public function getClickUpUserPhabdataByID( $id, $existing_data = null ) {
return $this->getClickUpUserByID( $id, $existing_data )->phabData;
}
public function getClickUpUserPHIDByID( $id, $existing_data = null ) {
return $this->getClickUpUserPhabdataByID( $id, $existing_data )->phid;
}
public function setClickUpUserPHIDByID( $id, $phid ) {
$this->getClickUpUserPhabdataByID( $id )->phid = $phid;
return $this;
}
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()
) );
}
}