├── .gitignore ├── install └── sql │ ├── remove_trello.sql │ ├── purge_trello_data.sql │ └── install_trello.sql ├── composer.json ├── plugin.php ├── class.trello_install.php ├── README.md ├── api.trello.php ├── config.php └── trello.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | composer.lock -------------------------------------------------------------------------------- /install/sql/remove_trello.sql: -------------------------------------------------------------------------------- 1 | DROP TRIGGER IF EXISTS `%TABLE_PREFIX%trello_config`$ -------------------------------------------------------------------------------- /install/sql/purge_trello_data.sql: -------------------------------------------------------------------------------- 1 | SET SQL_SAFE_UPDATES=0$ 2 | DELETE FROM `%TABLE_PREFIX%trello_config`$ 3 | SET SQL_SAFE_UPDATES=1$ 4 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "cdaguerre/php-trello-api": "@dev", 4 | "guzzlehttp/guzzle": "^6.2" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /install/sql/install_trello.sql: -------------------------------------------------------------------------------- 1 | SET SQL_SAFE_UPDATES=0$ 2 | 3 | CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%trello_config` ( 4 | `id` int(11) NOT NULL AUTO_INCREMENT, 5 | `key` varchar(255) NOT NULL DEFAULT 'undefined', 6 | `value` varchar(255) NOT NULL DEFAULT 'undefined', 7 | PRIMARY KEY (`id`), 8 | UNIQUE KEY `key_UNIQUE` (`key`) 9 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8$ 10 | 11 | 12 | SET SQL_SAFE_UPDATES=1$ 13 | -------------------------------------------------------------------------------- /plugin.php: -------------------------------------------------------------------------------- 1 | 'kyleladd:trello', # notrans 12 | 'version' => '0.1', 13 | 'name' => 'Trello Plugin', 14 | 'author' => 'Kyle Ladd', 15 | 'description' => 'Trello Plugin to have tickets be in sync with a Trello Board', 16 | 'url' => 'http://kyleladd.us', 17 | 'plugin' => 'trello.php:TrelloPlugin' 18 | ); 19 | 20 | ?> -------------------------------------------------------------------------------- /class.trello_install.php: -------------------------------------------------------------------------------- 1 | runJob ( $schemaFile ); 14 | } 15 | 16 | private function runJob($schemaFile, $show_sql_errors = true) { 17 | // Last minute checks. 18 | if (! file_exists ( $schemaFile )) { 19 | echo '
'; 20 | var_dump ( $schemaFile ); 21 | echo '
'; 22 | echo 'File Access Error - please make sure your download is the latest (#1)'; 23 | echo '
'; 24 | $this->error = 'File Access Error!'; 25 | return false; 26 | } elseif (! $this->load_sql_file ( $schemaFile, TABLE_PREFIX, true, true )) { 27 | if ($show_sql_errors) { 28 | echo '
'; 29 | echo 'Error parsing SQL schema! Get help from developers (#4)'; 30 | echo '
'; 31 | return false; 32 | } 33 | return true; 34 | } 35 | 36 | return true; 37 | } 38 | function remove() { 39 | $schemaFile = TRELLO_PLUGIN_ROOT . 'install/sql/remove_trello.sql'; // DB dump. 40 | return $this->runJob ( $schemaFile ); 41 | } 42 | function purgeData() { 43 | $schemaFile = TRELLO_PLUGIN_ROOT . 'install/sql/purge_trello_data.sql'; // DB dump. 44 | return $this->runJob ( $schemaFile ); 45 | } 46 | 47 | /** 48 | * Overriding split, we need semicolons in procedures and triggers, so 49 | * the dollar sign is used instead. 50 | * 51 | * @param type $schema 52 | * @param type $prefix 53 | * @param type $abort 54 | * @param type $debug 55 | * @return boolean 56 | */ 57 | function load_sql($schema, $prefix, $abort = true, $debug = false) { 58 | 59 | // Strip comments and remarks 60 | $schema = preg_replace ( '%^\s*(#|--).*$%m', '', $schema ); 61 | // Replace table prefix 62 | $schema = str_replace ( '%TABLE_PREFIX%', $prefix, $schema ); 63 | // Split by dollar signs - and cleanup 64 | if (! ($statements = array_filter ( array_map ( 'trim', 65 | // Thanks, http://stackoverflow.com/a/3147901 66 | preg_split ( "/\\$(?=(?:[^']*'[^']*')*[^']*$)/", $schema ) ) ))) 67 | return $this->abort ( 'Error parsing SQL schema', $debug ); 68 | 69 | db_query ( 'SET SESSION SQL_MODE =""', false ); 70 | foreach ( $statements as $k => $sql ) { 71 | if (db_query ( $sql, false )) 72 | continue; 73 | if (db_error () != null) { 74 | $error = "[$sql] " . db_error (); 75 | if ($abort) 76 | return $this->abort ( $error, $debug ); 77 | } 78 | } 79 | 80 | return true; 81 | } 82 | } 83 | 84 | ?> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OSTicket Trello Plugin 2 | The goal of this plugin is to be able to sync your OSTicket tickets with a Trello Board. 3 | 4 | ## Requirements 5 | - OSTicket v1.10 - slightly modified [https://github.com/kyleladd/osTicket/commits/OSTicketTrello](https://github.com/kyleladd/osTicket/commits/OSTicketTrello) 6 | - Support coming soon for unmodified OSTicket v1.9.14, v1.9.15 installs 7 | - Guzzle's Requirements: V6 (see composer.json) [http://docs.guzzlephp.org/en/stable/overview.html](http://docs.guzzlephp.org/en/stable/overview.html) 8 | - PHP 5.5 9 | 10 | ## Installation 11 | **Note**: For now, this plugin depends on OSTicket having an api which has not been merged in yet (PR [#2947](https://github.com/osTicket/osTicket/pull/2947)) as well as a PUT endpoint that I created. To see the modifications: [https://github.com/kyleladd/osTicket/commits/OSTicketTrello](https://github.com/kyleladd/osTicket/commits/OSTicketTrello) 12 | 13 | - ```composer install``` 14 | - Copy Repo to OSTicket's plugin directory (include/plugins/) 15 | - Add New Plugin: [OSTICKET_URL]/scp/plugins.php 16 | - Select the OSTicket Trello Plugin 17 | - Enable the plugin within OSTicket 18 | - Configure the plugin via the plugin's form. *Note: This must be completed after the plugin is enabled because Trello verifies the url is a 200 status code in order for the webhook to be created. Enabling the plugin first allows the plugin/OSTicket to answer the request with a 200 status code when configuring the plugin. When enabled, the plugin hooks into the url dispatcher and creates the api endpoints for Trello within OSTicket. "The provided callbackURL must be a valid URL during the creation of the webhook. We run a quick HTTP HEAD request on the URL, and if a 200 status code is not returned in the response, then the webhook will not be created." - https://developers.trello.com/apis/webhooks* 19 | - The webhook field will be automatically filled when the webhook is successfully created. 20 | 21 | ## Additional Configuration 22 | 23 | ### Updating ticket status 24 | Updating the ticket status is done by matching the name of the list (in Trello) to the name of the status (in OSTicket) 25 | 26 | #### Adding ticket statuses in OSTicket 27 | Admin Panel->Manage->Lists->Ticket Statuses->Add New Item 28 | 29 | - **Value**: Match the name of the list in Trello 30 | - **Item Properties**: Set the state of the ticket when it is (this status - OSTicket)/(in this list - Trello) 31 | 32 | ## Functionality 33 | ### Events triggered by Trello 34 | - Creating a card in Trello creates a ticket in OSTicket 35 | - Moving a card between lists in Trello updates the ticket status in OSTicket 36 | - Updating a card's description in Trello updates the ticket's description 37 | 38 | ### Events triggered by OSTicket 39 | - Creating a ticket in OSTicket creates a card in Trello 40 | 41 | ## Ticket Creation - Fields 42 | ### Action initiated in OSTicket -> Trello 43 | | Action | Create | 44 | | ------------- | ------------- | 45 | | Title | X | 46 | | Description | X | 47 | | Status | X | 48 | | Due Date | | 49 | | Attachment | | 50 | 51 | ### Action initiated in Trello -> OSTicket 52 | | Action | Create | 53 | | ------------- | ------------- | 54 | | Title | X | 55 | | Description | X | 56 | | Status | X | 57 | 58 | ## Syncing 59 | ### Action initiated in OSTicket 60 | | Action | Create | Update | Delete | 61 | | ------------- | ------------- | ------------- | ------------- | 62 | | Ticket | X | NA | | 63 | | Title | NA | | | 64 | | Description | NA | X | | 65 | | Status | NA | X | NA | 66 | | Due Date | | | | 67 | | Public Comment | X | | | 68 | | Internal Comment | | | | 69 | | Attachment | | | | 70 | | Tasks | | | | 71 | 72 | ### Action initiated in Trello 73 | | Action | Create | Update | Delete | 74 | | ------------- | ------------- | ------------- | ------------- | 75 | | Ticket | X | NA | | 76 | | Title | NA | | NA | 77 | | Description | NA | X | NA | 78 | | Status | NA | X | NA | 79 | | Due Date | NA | | | 80 | | Public Comment | X | | | 81 | | Internal Comment | | | | 82 | | Attachment | | | | 83 | | Tasks | | | | 84 | -------------------------------------------------------------------------------- /api.trello.php: -------------------------------------------------------------------------------- 1 | response(200, json_encode("Hello World from Trello Plugin."), 11 | $contentType="application/json"); 12 | } 13 | function postFromTrello(){ 14 | try{ 15 | global $ost, $cfg; 16 | $config = TrelloPlugin::getConfig(); 17 | $errors = array(); 18 | $ticket = null; 19 | // https://developers.trello.com/apis/webhooks 20 | // HTTP_X_REAL_IP 21 | $json = $this->getRequest('json'); 22 | if(!TrelloPlugin::isvalidTrelloIP($_SERVER['HTTP_X_REAL_IP'])){ 23 | $this->response(401, json_encode("Bad IP"), 24 | $contentType="application/json"); 25 | } 26 | // TODO - if not valid webhook/listId Trello Model Id, return 401 27 | if($json['model']['id'] !== $config->get('trello_board_id')){ 28 | $this->response(401, json_encode("Trello list id does not match what is stored in OSTicket"), 29 | $contentType="application/json"); 30 | } 31 | 32 | // For matching the Trello list names to the status names 33 | $statusesOrig = TicketStatusList::getStatuses(array('states' => $states))->all(); 34 | $statuses = array(); 35 | foreach ($statusesOrig as $status){ 36 | $statuses[$status->getId()] = $status->getName(); 37 | } 38 | 39 | $client = new Client(); 40 | $client->authenticate($config->get('trello_api_key'), $config->get('trello_api_token'), Client::AUTH_URL_CLIENT_ID); 41 | $manager = new Manager($client); 42 | 43 | if($json['action']['type']==="createCard"){ 44 | $ticket_id = TrelloPlugin::parseTrelloTicketNumber($json['action']['data']['card']['name']); 45 | if($ticket_id != null){ 46 | $ticket = Ticket::lookup($ticket_id); 47 | } 48 | // Ticket creation was initiated from trello 49 | if($ticket == null){ 50 | // $duedate = () ? : ""; 51 | $duedate = ""; 52 | $subject = $json['action']['data']['card']['name']; 53 | $statusId = array_search($json['action']['data']['list']['name'],$statuses); 54 | $card = $manager->getCard($json['action']['data']['card']['id']); 55 | $desc = $card->getDescription(); 56 | 57 | if(!empty($desc)){ 58 | $message = $desc; 59 | } 60 | else{ 61 | $message = "Card was created in Trello, description is coming soon. The card is located: https://trello.com/c/".$json['action']['data']['card']['shortLink'].""; 62 | } 63 | 64 | $ticketToBeCreated = array( 65 | "subject" => "***OSTICKETPLUGIN***".$subject, 66 | "message" => $message, 67 | "duedate" => $duedate, 68 | "statusId" => $statusId, 69 | // "source" => "Trello", 70 | "source" => "Other", 71 | "email" => $config->get('trello_user_email') 72 | ); 73 | 74 | $ticket = Ticket::create($ticketToBeCreated, $errors, "api", false, false); 75 | if($ticket == null || !empty($errors)){ 76 | $ost->logDebug("DEBUG","Can't create ticket. ". json_encode($json)); 77 | $this->response(500, json_encode($errors), 78 | $contentType="application/json"); 79 | } 80 | $entries = $ticket->getThread()->getEntries(); 81 | $ticket->_answers['subject'] = $ticket->getId() . " - " . $subject; 82 | 83 | foreach (DynamicFormEntryAnswer::objects() 84 | ->filter(array( 85 | 'entry__object_id' => $ticket->getId(), 86 | 'entry__object_type' => 'T' 87 | )) as $answer 88 | ) { 89 | if(mb_strtolower($answer->field->name) 90 | ?: 'field.' . $answer->field->id == "subject"){ 91 | $answer->setValue($ticket->getId() . " - " . $subject); 92 | $answer->save(); 93 | } 94 | } 95 | foreach ($entries as $entry) { 96 | $entry->title = $ticket->getId() . " - " . $subject; 97 | $entry->save(); 98 | } 99 | $client->cards()->setName($json['action']['data']['card']['id'], $ticket->getSubject()); 100 | } 101 | } 102 | // If it is a card being moved into a new list, 103 | elseif($json['action']['type']==="updateCard"){ 104 | // Get matching ticket to the card that was updated 105 | $ticket_id = TrelloPlugin::parseTrelloTicketNumber($json['action']['data']['card']['name']); 106 | if($ticket_id == null){ 107 | $ost->logDebug("DEBUG","Can't parse ticket. ". json_encode($json)); 108 | $this->response(404, json_encode("Unable to parse ticket id"), 109 | $contentType="application/json"); 110 | } 111 | $ticket = Ticket::lookup($ticket_id); 112 | if($ticket == null){ 113 | $ost->logDebug("DEBUG","Can't find ticket. ". json_encode($json)); 114 | $this->response(404, json_encode("Unable to find ticket."), 115 | $contentType="application/json"); 116 | } 117 | 118 | // If we are moving between lists - Updating the ticket status 119 | if(isset($json['action']['data']['listAfter'])){ 120 | $status = array_search($json['action']['data']['listAfter']['name'],$statuses); 121 | if(!empty($status) && $ticket->getStatusId() != $status){ 122 | if($ticket->setStatus($status)){ 123 | $this->response(200, json_encode($ticket), 124 | $contentType="application/json"); 125 | } 126 | else{ 127 | $ost->logDebug("DEBUG","Can't update ticket. ". json_encode($json)); 128 | $this->response(500, json_encode("Unable to update ticket status"), 129 | $contentType="application/json"); 130 | } 131 | } 132 | // If there is a matching OSTicket status, update ticket status to Trello list as status 133 | } 134 | // Update the ticket description 135 | // TODO - verify \n\r\t are maintained during syncing 136 | if(isset($json['action']['data']['card']['desc']) && $ticket->getThreadEntries()[0]->getBody()->body !== $json['action']['data']['card']['desc']){ 137 | $ticket->getThreadEntries()[0]->setBody($json['action']['data']['card']['desc']); 138 | } 139 | } 140 | // If comment was added to card 141 | elseif($json['action']['type']==="commentCard"){ 142 | $ticket = TrelloPlugin::getOSTicketFromTrelloHook($json); 143 | $ticket->getThread()->addResponse(array("threadId"=>$ticket->getThreadId(), "response"=>$json['action']['data']['text']), $errors); 144 | } 145 | if(!empty($errors)){ 146 | $ost->logDebug("DEBUG","Errors: ". json_encode($errors)); 147 | $this->response(500, json_encode($errors), 148 | $contentType="application/json"); 149 | } 150 | else{ 151 | $this->response(200, "Ticket updated", 152 | $contentType="application/json"); 153 | } 154 | } 155 | catch(Exception $e){ 156 | $ost->logDebug("DEBUG","Error post from Trello. " . $e->getMessage()); 157 | $this->response(500, json_encode($e->getMessage()), 158 | $contentType="application/json"); 159 | } 160 | } 161 | } 162 | ?> 163 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | field->getConfiguration(); 27 | if (isset($config['validate_choices']) && $config['validate_choices'] == false) { 28 | $values = array_flip($value); 29 | } 30 | else{ 31 | $choices = $this->field->getChoices(); 32 | if (is_array($value)) { 33 | foreach($value as $k => $v) { 34 | if (isset($choices[$v])) 35 | $values[$v] = $choices[$v]; 36 | } 37 | } 38 | } 39 | return $values; 40 | } 41 | } 42 | class TrelloConfig extends PluginConfig{ 43 | function hasCustomConfig(){ 44 | return true; 45 | } 46 | function renderCustomConfig(){ 47 | $form = $this->getForm(); 48 | $form->isValid(); 49 | ?> 50 | 84 | render(); 86 | } 87 | 88 | function saveCustomConfig(){ 89 | try{ 90 | global $cfg; 91 | $client = new Client(); 92 | $config = TrelloPlugin::getConfig(); 93 | // Initial board 94 | $initial_board = $config->get('trello_board_id'); 95 | $initial_webhook = $config->get('trello_webhook_id'); 96 | $form = $this->getForm(); 97 | $form->getField('trello_webhook_id')->value; 98 | $create_new_webhook = true; 99 | if(!empty($form->getField('trello_webhook_id')->value) && $form->getField('trello_webhook_id')->value !== $initial_webhook){ 100 | // Webhook ID was manually entered and not generated 101 | // Check to see if entered webhook id is an already used webhook for this endpoint - verification 102 | // $client->authenticate($form->getField('trello_api_key')->value, $form->getField('trello_api_token')->value, Client::AUTH_URL_CLIENT_ID); 103 | // $client->webhooks()->get() 104 | $create_new_webhook = false; 105 | } 106 | 107 | if($this->commitForm()){ 108 | $config = TrelloPlugin::getConfig(); 109 | $saved_board = $config->get('trello_board_id'); 110 | // Create webhook for new board and remove webhook from old board if there is one 111 | if($saved_board !== $initial_board || empty($config->get('trello_webhook_id'))){ 112 | $client->authenticate($config->get('trello_api_key'), $config->get('trello_api_token'), Client::AUTH_URL_CLIENT_ID); 113 | if(!empty($initial_webhook)){ 114 | // Remove existing webhook 115 | try{ 116 | $client->webhooks()->remove($config->get('trello_webhook_id')); 117 | } 118 | catch(Exception $e){ 119 | error_log("Unable to delete Trello Webhook. " . $e->getMessage()); 120 | echo "Unable to delete Trello Webhook."; 121 | var_dump($e); 122 | } 123 | } 124 | if($create_new_webhook){ 125 | $trello_webhook_create = $client->webhooks()->create(array("idModel"=>$saved_board,"callbackURL"=>$cfg->getBaseUrl() . "/api/trello","description"=>"OSTicket Plugin")); 126 | if(TrelloConfig::update('trello_webhook_id',$trello_webhook_create['id']) === false){ 127 | echo "Failed to save created webhook to database"; 128 | return false; 129 | } 130 | } 131 | } 132 | 133 | } 134 | } 135 | catch(Exception $e){ 136 | error_log("Error authenticating to Trello. " . $e->getMessage()); 137 | var_dump($e); 138 | return false; 139 | } 140 | return true; 141 | } 142 | 143 | function getOptions() { 144 | return array( 145 | 'trello_api_key' => new TextboxField(array( 146 | 'id' => 'trello_api_key', 147 | 'label' => 'Trello API Key', 148 | 'required'=>true, 149 | 'hint'=>__('Get your Key: https://trello.com/app-key'), 150 | 'configuration' => array( 151 | 'length' => 0 152 | ) 153 | )), 154 | 'trello_api_token' => new TextboxField(array( 155 | 'id' => 'trello_api_token', 156 | 'label' => 'Trello API Token', 157 | 'required'=>true, 158 | 'hint'=>__('Get your Token: https://trello.com/1/authorize?key=APPLICATIONKEYHERE&scope=read%2Cwrite&name=My+Application&expiration=never&response_type=token'), 159 | 'configuration' => array( 160 | 'length' => 0 161 | ), 162 | )), 163 | 'trello_board_id' => new OptionalValidationChoiceField(array( 164 | 'id' => 'trello_board_id', 165 | 'label' => 'Trello Board ID', 166 | 'required'=>true, 167 | 'hint'=>__('Get your Token: https://trello.com/1/authorize?key=APPLICATIONKEYHERE&scope=read%2Cwrite&name=My+Application&expiration=never&response_type=token'), 168 | 'configuration' => array( 169 | 'multiselect' => false, 170 | 'validate_choices' => false 171 | ), 172 | )), 173 | 'trello_list_id' => new OptionalValidationChoiceField(array( 174 | 'id' => 'trello_list_id', 175 | 'label' => 'Trello Creation List ID', 176 | 'required'=>true, 177 | 'hint'=>__('When a ticket is created, add card to this list in Trello'), 178 | 'configuration' => array( 179 | 'multiselect' => false, 180 | 'validate_choices' => false 181 | ), 182 | )), 183 | 'osticket_department_id' => new ChoiceField(array( 184 | 'id'=>'osticket_department_id', 185 | 'label'=>__('Department'), 186 | 'required'=>true, 187 | 'hint'=>__('When a ticket is created for this department, create the corresponding card in Trello.'), 188 | 'choices'=>Dept::getDepartments(), 189 | 'configuration'=>array( 190 | 'multiselect' => false 191 | ) 192 | )), 193 | 'trello_user_email' => new TextboxField(array( 194 | 'id' => 'trello_user_email', 195 | 'label' => 'OSTicket User Email for Trello', 196 | 'required'=>true, 197 | 'hint'=>__('For Ticket creation in Trello - Just needs to be an email address that has already created at least one ticket in OSTicket. This email address will create all Trello-created tickets.'), 198 | 'configuration' => array( 199 | 'length' => 0 200 | ), 201 | )), 202 | 'trello_webhook_id' => new TextboxField(array( 203 | 'id' => 'trello_webhook_id', 204 | 'label' => 'Current Trello Webhook', 205 | 'required'=>false, 206 | 'hint'=>__('Generated and used for webhook removal. However, if you\'ve previously created a webhook for this plugin for this url, you can manually enter it. To see your current webhooks: https://developers.trello.com/sandbox -> Get Webhooks'), 207 | 'configuration' => array( 208 | 'length' => 0 209 | ), 210 | )) 211 | ); 212 | } 213 | 214 | function pre_save(&$config, &$errors) { 215 | global $msg; 216 | 217 | if (!$errors) 218 | $msg = 'Configuration updated successfully'; 219 | 220 | return true; 221 | } 222 | } 223 | ?> 224 | -------------------------------------------------------------------------------- /trello.php: -------------------------------------------------------------------------------- 1 | firstRun()) { 22 | $this->configureFirstRun(); 23 | } 24 | 25 | $config = $this->getConfig(); 26 | Signal::connect ( 'api', array('TrelloPlugin', 'callbackDispatch' )); 27 | Signal::connect('ticket.created', array($this, 'onTicketCreated'), 'Ticket'); 28 | Signal::connect('model.updated', array($this, 'onModelUpdated')); 29 | Signal::connect('model.created', array($this, 'onModelCreated')); 30 | } 31 | 32 | /** 33 | * Checks if this is the first run of our plugin. 34 | * @return boolean 35 | */ 36 | function firstRun() { 37 | $sql='SHOW TABLES LIKE \''.TRELLO_TABLE.'\''; 38 | $res=db_query($sql); 39 | return (db_num_rows($res)==0); 40 | } 41 | 42 | /** 43 | * Necessary functionality to configure first run of the application 44 | */ 45 | function configureFirstRun() { 46 | if(!$this->createDBTables()) 47 | { 48 | echo "First run configuration error. " 49 | . "Unable to create database tables!"; 50 | } 51 | } 52 | 53 | /** 54 | * Kicks off database installation scripts 55 | * @return boolean 56 | */ 57 | function createDBTables() { 58 | $installer = new TrelloInstaller(); 59 | return $installer->install(); 60 | } 61 | 62 | /** 63 | * Uninstall hook. 64 | * @param type $errors 65 | * @return boolean 66 | */ 67 | function pre_uninstall(&$errors) { 68 | $installer = new TrelloInstaller(); 69 | return $installer->remove(); 70 | } 71 | 72 | function onTicketCreated($ticket){ 73 | //If it did not come from trello plugin API 74 | if(!strpos($ticket->getSubject(), "***OSTICKETPLUGIN***") === 0){ 75 | // if($ticket->getSource() != "Trello"){ 76 | try{ 77 | $config = $this->getConfig(); 78 | // If the ticket was made for the department with a hook into Trello 79 | if($config->get('osticket_department_id') == $ticket->dept->id){ 80 | // TRELLO CHANGES ON TICKET CREATION 81 | $client = new Client(); 82 | $client->authenticate($config->get('trello_api_key'), $config->get('trello_api_token'), Client::AUTH_URL_CLIENT_ID); 83 | // // POST to Trello 84 | $newcard = array("idList"=> $config->get('trello_list_id'), "name"=>TrelloPlugin::createTrelloTitle($ticket), "desc"=>$ticket->getLastMessage()->getBody()); 85 | $client->cards()->create($newcard); 86 | } 87 | } 88 | catch(Exception $e){ 89 | error_log("Error posting to Trello. " . $e->getMessage()); 90 | } 91 | // } 92 | } 93 | } 94 | 95 | function onModelUpdated($object, $data){ 96 | // $data is the old data that was before being changed 97 | // $object is the object with the updated data 98 | // A Ticket was updated 99 | if(get_class($object) === "Ticket"){ 100 | $ticket = $object; 101 | // Authenticate to Trello 102 | $config = $this->getConfig(); 103 | $client = new Client(); 104 | $client->authenticate($config->get('trello_api_key'), $config->get('trello_api_token'), Client::AUTH_URL_CLIENT_ID); 105 | 106 | // If the status was updated 107 | if(isset($data['dirty']['status_id'])){ 108 | // $data['dirty']['status_id'] - is the old status id 109 | // $ticket->getStatusId(); is the new status id 110 | 111 | // Matching the status in OSTicket to a List in Trello 112 | $trelloCardId = TrelloPlugin::getTrelloCardId($ticket, $client, $config); 113 | $trelloListId = TrelloPlugin::getTrelloListId($ticket->getStatusId(), $client, $config); 114 | if(!empty($trelloCardId) && !empty($trelloListId)){ 115 | // Updating the list/status in Trello 116 | $client->cards()->setList($trelloCardId, $trelloListId); 117 | } 118 | } 119 | } 120 | } 121 | 122 | function onModelCreated($object, $data){ 123 | if(get_class($object) === "ResponseThreadEntry"){ 124 | // Creating a new response to a ticket 125 | $response = $object; 126 | $config = $this->getConfig(); 127 | $client = new Client(); 128 | $client->authenticate($config->get('trello_api_key'), $config->get('trello_api_token'), Client::AUTH_URL_CLIENT_ID); 129 | // print_r($response->getBody()->getClean()); 130 | // print_r($response->getBody()->display()); 131 | $text = Format::htmldecode($response->getBody()->getClean()); 132 | //Get card in trello 133 | $ticket = $response->getThread()->getObject(); // gets the ticket, class.thread.php 134 | $cardId = TrelloPlugin::getTrelloCardId($ticket, $client, $config); 135 | if(!empty($cardId)){ 136 | $trelloComments = TrelloPlugin::getCardComments($cardId, $client); 137 | // if card does not have matching comment, post to trello 138 | if(empty(TrelloPlugin::searchArrayByInnerProperty($trelloComments, "data.text", $text))){ 139 | $client->cards()->actions()->addComment($cardId, $text); 140 | } 141 | } 142 | } 143 | elseif(get_class($object) === "ThreadEntry"){ 144 | // Updating a response or the ticket's description 145 | $entry = $object; 146 | $config = $this->getConfig(); 147 | $client = new Client(); 148 | $client->authenticate($config->get('trello_api_key'), $config->get('trello_api_token'), Client::AUTH_URL_CLIENT_ID); 149 | $manager = new Manager($client); 150 | $ticket = $entry->getThread()->getObject(); 151 | $cardId = TrelloPlugin::getTrelloCardId($ticket, $client, $config); 152 | if(!empty($cardId)){ 153 | $trelloComments = TrelloPlugin::getCardComments($cardId, $client); 154 | // Updating the ticket's description/first entry - nope, this is any entry 155 | if($entry->getType() === "M"){ 156 | $desc = Format::htmldecode($entry->getBody()->getClean()); 157 | if(!empty($cardId)){ 158 | $trelloCard = $manager->getCard($cardId); 159 | if($desc !== $trelloCard->getDescription()){ 160 | $trelloCard->setDescription($desc)->save(); 161 | } 162 | } 163 | } 164 | elseif($entry->getType() === "R"){ 165 | $text = Format::htmldecode($entry->getBody()->getClean()); 166 | // Updating a response 167 | // if this is a new reply 168 | if(empty($entry->getPid())){ 169 | // if card does not have matching comment, post to trello 170 | if(empty(TrelloPlugin::searchArrayByInnerProperty($trelloComments, "data.text", $text))){ 171 | $client->cards()->actions()->addComment($cardId, $text); 172 | } 173 | } 174 | else{ 175 | // It is an edit to a reply 176 | $originalEntry = ResponseThreadEntry::lookup($entry->getPid()); 177 | $originalText = Format::htmldecode($originalEntry->getBody()->getClean()); 178 | $originalComment = TrelloPlugin::searchArrayByInnerProperty($trelloComments, "data.text", $originalText); 179 | //update comment 180 | if(!empty($originalComment)){ 181 | // $client->cards()->actions()->removeComment($cardId, $originalComment['id']); 182 | $client->actions()->setText($originalComment['id'], $text); 183 | } 184 | else{ 185 | //add new comment 186 | $client->cards()->actions()->addComment($cardId, $text); 187 | } 188 | } 189 | } 190 | } 191 | } 192 | } 193 | 194 | static function createTrelloTitle($ticket){ 195 | return $ticket->getId() . " - " . $ticket->getSubject(); 196 | } 197 | 198 | static function parseTrelloTicketNumber($title){ 199 | try{ 200 | $ticketNumber = substr ( $title , 0, strpos ( $title , "-" ) - 1 ); 201 | if(TrelloPlugin::isInteger($ticketNumber)){ 202 | return $ticketNumber; 203 | } 204 | } 205 | catch(Exception $e){ 206 | } 207 | return null; 208 | } 209 | 210 | public static function getOSTicketFromTrelloHook($json){ 211 | $ticket = null; 212 | try{ 213 | $ticket_id = TrelloPlugin::parseTrelloTicketNumber($json['action']['data']['card']['name']); 214 | if(!empty($ticket_id)){ 215 | $ticket = Ticket::lookup($ticket_id); 216 | } 217 | } 218 | catch(Exception $e){ 219 | } 220 | return $ticket; 221 | } 222 | 223 | // Add new Routes 224 | public static function callbackDispatch($object, $data) { 225 | $object->append(url_post('^/trello$', array('TrelloApiController', 'postFromTrello'))); 226 | $object->append(url('^/trello$', array('TrelloApiController', 'allFromTrello'))); 227 | } 228 | 229 | // https://developers.trello.com/apis/webhooks#source 230 | public static function isValidTrelloIP($ip){ 231 | $trelloIPs = array("107.23.104.115","107.23.149.70","54.152.166.250","54.164.77.56"); 232 | return in_array($ip,$trelloIPs); 233 | } 234 | 235 | public static function getTrelloListId($osticketStatusId, $client, $config){ 236 | try{ 237 | //Get all statuses 238 | $statusesOrig = TicketStatusList::getStatuses(array('states' => $states))->all(); 239 | $statuses = array(); 240 | foreach ($statusesOrig as $status){ 241 | $statuses[$status->getId()] = $status->getName(); 242 | } 243 | $statusName = $statuses[$osticketStatusId]; 244 | // get trello lists for board 245 | // Lists 246 | $trelloLists = $client->boards()->lists()->all($config->get('trello_board_id')); 247 | // Match based on names 248 | $matchingTrelloList = TrelloPlugin::searchArrayByProperty($trelloLists,'name',$statusName); 249 | // https://developers.trello.com/advanced-reference/board#get-1-boards-board-id-lists 250 | return $matchingTrelloList['id']; 251 | } 252 | catch(Exception $e){ 253 | return null; 254 | } 255 | } 256 | public static function getCardComments($cardId, $client){ 257 | try{ 258 | if(!empty($cardId)){ 259 | return $client->cards()->actions()->all($cardId,array("filter" => "commentCard")); 260 | } 261 | } 262 | catch(Exception $e){ 263 | 264 | } 265 | return null; 266 | } 267 | 268 | public static function getTrelloCardId($ticket, $client, $config){ 269 | try{ 270 | $trelloCards = $client->boards()->cards()->all($config->get('trello_board_id')); 271 | $matchingTrelloCard = TrelloPlugin::searchArrayByProperty($trelloCards,'name',TrelloPlugin::createTrelloTitle($ticket)); 272 | return $matchingTrelloCard['id']; 273 | // https://developers.trello.com/advanced-reference/board#get-1-boards-board-id-cards 274 | } 275 | catch(Exception $e){ 276 | return null; 277 | } 278 | } 279 | 280 | public static function searchArrayByProperty($array,$property,$value){ 281 | try{ 282 | $item = null; 283 | foreach($array as $struct) { 284 | if ($value == $struct[$property]) { 285 | $item = $struct; 286 | break; 287 | } 288 | } 289 | return $item; 290 | } 291 | catch(Exception $e){ 292 | return null; 293 | } 294 | } 295 | 296 | public static function searchArrayByInnerProperty($array,$property,$value){ 297 | try{ 298 | if(is_string($property)){ 299 | $property = explode(".",$property); 300 | } 301 | $item = null; 302 | foreach($array as $struct) { 303 | $searchableItem = $struct; 304 | for ($i = 0; $i < count($property); $i++) { 305 | if($i === count($property) - 1){ 306 | //Check value 307 | if ($value == $searchableItem[$property[$i]]) { 308 | $item = $struct; 309 | break 2; // break the foreach and the for loop 310 | } 311 | } 312 | else{ 313 | $searchableItem = $searchableItem[$property[$i]]; 314 | } 315 | } 316 | } 317 | return $item; 318 | } 319 | catch(Exception $e){ 320 | return null; 321 | } 322 | } 323 | public static function isInteger($input){ 324 | return (ctype_digit(strval($input)) && !empty($input)); 325 | } 326 | } --------------------------------------------------------------------------------