├── .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 | }
--------------------------------------------------------------------------------