diff --git a/README.md b/README.md index a3d3c31..f1ee997 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ This plugin uses CURL and tested on osTicket-1.10.1 - Select "Authorize" - Scroll down and copy the Webhook URL entirely, paste this into the Plugin config. +If you want to add the Department as a field in each slack notice, tick the Checkbox in the Plugin config. + The channel you select will receive an event notice, like: ``` Aaron [10:56 AM] added an integration to this channel: osTicket Notification @@ -37,10 +39,10 @@ Create a ticket! You should see something like the following appear in your Slack channel: -![slack-new-ticket](https://user-images.githubusercontent.com/5077391/31572028-5b8f69ce-b0e8-11e7-86f0-d5a4cef2b98e.png) +![slack-new-ticket](https://user-images.githubusercontent.com/5077391/31572647-923e07b0-b0f6-11e7-9515-98205d6f800f.png) When a user replies, you'll get something like: -![slack-reply](https://user-images.githubusercontent.com/5077391/31572029-5d1144e8-b0e8-11e7-9fad-cc5204b0ca64.png) +![slack-reply](https://user-images.githubusercontent.com/5077391/31572648-9279eb18-b0f6-11e7-97da-9a9c63a200d4.png) Notes, Replies from Agents and System messages shouldn't appear. \ No newline at end of file diff --git a/config.php b/config.php index 39c1baf..0b9b400 100644 --- a/config.php +++ b/config.php @@ -35,6 +35,10 @@ function getOptions() { 'length' => 200 ), )), + 'display-dept' => new BooleanField([ + 'label' => $__('Add field for Department in Slack notice'), + 'default' => FALSE, + ]) ); } diff --git a/slack.php b/slack.php index 7c382e7..277a378 100644 --- a/slack.php +++ b/slack.php @@ -5,17 +5,30 @@ require_once(INCLUDE_DIR . 'class.ticket.php'); require_once(INCLUDE_DIR . 'class.osticket.php'); require_once(INCLUDE_DIR . 'class.config.php'); +require_once(INCLUDE_DIR . 'class.format.php'); require_once('config.php'); class SlackPlugin extends Plugin { var $config_class = "SlackPluginConfig"; + /** + * The entrypoint of the plugin, keep short, always runs. + */ function bootstrap() { + // Listen for osTicket to tell us it's made a new ticket or updated + // an existing ticket: Signal::connect('ticket.created', array($this, 'onTicketCreated')); Signal::connect('threadentry.created', array($this, 'onTicketUpdated')); } + /** + * What to do with a new Ticket? + * + * @global OsticketConfig $cfg + * @param Ticket $ticket + * @return type + */ function onTicketCreated(Ticket $ticket) { global $cfg; if (!$cfg instanceof OsticketConfig) { @@ -26,6 +39,7 @@ function onTicketCreated(Ticket $ticket) { // Convert any HTML in the message into text $plaintext = Format::html2text($ticket->getMessages()[0]->getBody()->getClean()); + // Format the messages we'll send. $heading = sprintf('%s CONTROLSTART%sscp/tickets.php?id=%d|#%s - %sCONTROLEND %s' , __("New Ticket") , $cfg->getBaseUrl() @@ -38,9 +52,16 @@ function onTicketCreated(Ticket $ticket) { , $ticket->getName() , $ticket->getEmail() , "\n\n" . $plaintext); - $this->sendToSlack($ticket, $heading, $body); + $this->sendToSlack($ticket, $heading, $plaintext); } + /** + * What to do with an Updated Ticket? + * + * @global OsticketConfig $cfg + * @param ThreadEntry $entry + * @return type + */ function onTicketUpdated(ThreadEntry $entry) { global $cfg; if (!$cfg instanceof OsticketConfig) { @@ -51,16 +72,20 @@ function onTicketUpdated(ThreadEntry $entry) { // this was a reply or a system entry.. not a message from a user return; } - $ticket = $this->getTicket($entry); + + // Need to fetch the ticket from the ThreadEntry + $ticket = $this->getTicket($entry); + + // Check to make sure this entry isn't the first (ie: a New ticket) $first_entry = $ticket->getMessages()[0]; if ($entry->getId() == $first_entry->getId()) { - // don't post the same thing twice.. let onCreated handle it. return; } // Convert any HTML in the message into text $plaintext = Format::html2text($entry->getBody()->getClean()); + // Format the messages we'll send $heading = sprintf('%s CONTROLSTART%sscp/tickets.php?id=%d|#%s %sCONTROLEND %s' , __("Ticket") , $cfg->getBaseUrl() @@ -75,47 +100,90 @@ function onTicketUpdated(ThreadEntry $entry) { , __('in') , $ticket->getDeptName() , "\n\n" . $plaintext); - $this->sendToSlack($ticket, $heading, $body, 'warning'); + $this->sendToSlack($ticket, $heading, $plaintext, 'warning'); } + /** + * A helper function that sends messages to slack endpoints. + * + * @global osTicket $ost + * @global OsticketConfig $cfg + * @param Ticket $ticket + * @param string $heading + * @param string $body + * @param string $colour + * @throws \Exception + */ function sendToSlack(Ticket $ticket, $heading, $body, $colour = 'good') { global $ost, $cfg; if (!$ost instanceof osTicket || !$cfg instanceof OsticketConfig) { error_log("Slack plugin called too early."); return; } + $url = $this->getConfig()->get('slack-webhook-url'); + if (!$url) { + $ost->logError('Slack Plugin not configured', 'You need to read the Readme and configure a webhook URL before using this.'); + } - // Obey message formatting rules:https://api.slack.com/docs/message-formatting - $formatter = ['<' => '<', '>' => '>', '&' => '&']; - $heading = str_replace(array_keys($formatter), array_values($formatter), $heading); - $body = str_replace(array_keys($formatter), array_values($formatter), $body); - // put the <>'s control characters back in - $moreformatter = ['CONTROLSTART' => '<', 'CONTROLEND' => '>']; - $heading = str_replace(array_keys($moreformatter), array_values($moreformatter), $heading); - $body = str_replace(array_keys($moreformatter), array_values($moreformatter), $body); + $heading = $this->format_text($heading); + $body = $this->format_text($body); - try { - $payload['attachments'][] = [ - 'pretext' => $heading, - 'fallback' => $heading, - 'color' => $colour, - "author" => $ticket->getName(), - "author_link" => $cfg->getBaseUrl() . 'scp/users.php?id=' . $ticket->getOwner()->getId(), - 'ts' => Misc::gmtime(), - 'footer' => __('Department') . ': ' . $ticket->getDeptName() . ' -> ' . $ticket->getTopic(), - "fields" => [ - [ - "title" => $ticket->getSubject(), - "title_link" => $cfg->getBaseUrl() . 'scp/tickets.php?id=' . $ticket->getId(), - "value" => $body, - "short" => false, - ] - ] + // Build the payload with the formatted data: + $payload['attachments'][0] = [ + 'pretext' => $heading, + 'fallback' => $heading, + 'color' => $colour, + 'title' => $ticket->getSubject(), + 'title_link' => $cfg->getBaseUrl() . 'scp/tickets.php?id=' . $ticket->getId(), + 'ts' => Misc::gmtime(), + 'footer' => 'via osTicket Slack Plugin', + 'footer_icon' => 'https://platform.slack-edge.com/img/default_application_icon.png', + 'text' => $body, + 'fields' => [ + [ + 'title' => __('User'), + 'value' => '<' . $cfg->getBaseUrl() . 'scp/users.php?id=' . $ticket->getOwnerId() . '|' . $ticket->getName() . '> (' . $ticket->getEmail() . ')', + 'short' => TRUE, + ], + [ + 'title' => __('Priority'), + 'value' => $ticket->getPriority(), + 'short' => TRUE, + ], + [ + 'title' => __('Topic'), + 'value' => str_replace('/', '-', $ticket->getTopic()), + 'short' => TRUE, + ], + ] + ]; + // Add a field for tasks if there are open ones + if ($ticket->getNumOpenTasks()) { + $payload['attachments'][0]['fields'][] = [ + 'title' => __('Open Tasks'), + 'value' => $ticket->getNumOpenTasks(), + 'short' => TRUE, ]; + } + // Change the colour to Fuschia if ticket is overdue + if ($ticket->isOverdue()) { + $payload['attachments'][0]['colour'] = '#ff00ff'; + } - $data_string = utf8_encode(json_encode($payload)); - $url = $this->getConfig()->get('slack-webhook-url'); + if ($this->getConfig()->get('display-dept')) { + $payload['attachments'][0]['fields'][] = [ + 'title' => __('Department'), + 'value' => $ticket->getDeptName(), + 'short' => TRUE, + ]; + } + + + // Format the payload: + $data_string = utf8_encode(json_encode($payload)); + try { + // Setup curl $ch = curl_init($url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); @@ -125,18 +193,23 @@ function sendToSlack(Ticket $ticket, $heading, $body, $colour = 'good') { 'Content-Length: ' . strlen($data_string)) ); + // Actually send the payload to slack: if (curl_exec($ch) === false) { throw new \Exception($url . ' - ' . curl_error($ch)); } else { $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($statusCode != '200') { - throw new \Exception($url . ' Http code: ' . $statusCode); + throw new \Exception( + 'Error sending to: ' . $url + . ' Http code: ' . $statusCode + . ' curl-error: ' . curl_errno($ch)); } } - curl_close($ch); } catch (\Exception $e) { $ost->logError('Slack posting issue!', $e->getMessage(), true); error_log('Error posting to Slack. ' . $e->getMessage()); + } finally { + curl_close($ch); } } @@ -146,20 +219,40 @@ function sendToSlack(Ticket $ticket, $heading, $body, $colour = 'good') { * @param ThreadEntry $entry * @return Ticket */ - private static function getTicket(ThreadEntry $entry) { - static $ticket; - if (!$ticket) { - // aquire ticket from $entry.. I suspect there is a more efficient way. - $ticket_id = Thread::objects()->filter([ - 'id' => $entry->getThreadId() - ])->values_flat('object_id')->first() [0]; - - // Force lookup rather than use cached data.. - $ticket = Ticket::lookup(array( - 'ticket_id' => $ticket_id - )); - } - return $ticket; + function getTicket(ThreadEntry $entry) { + $ticket_id = Thread::objects()->filter([ + 'id' => $entry->getThreadId() + ])->values_flat('object_id')->first() [0]; + + // Force lookup rather than use cached data.. + // This ensures we get the full ticket, with all + // thread entries etc.. + return Ticket::lookup(array( + 'ticket_id' => $ticket_id + )); + } + + /** + * Formats text according to the + * formatting rules:https://api.slack.com/docs/message-formatting + * + * @param string $text + * @return string + */ + function format_text($text) { + $formatter = [ + '<' => '<', + '>' => '>', + '&' => '&' + ]; + $formatted_text = str_replace(array_keys($formatter), array_values($formatter), $text); + // put the <>'s control characters back in + $moreformatter = [ + 'CONTROLSTART' => '<', + 'CONTROLEND' => '>' + ]; + // Replace the CONTROL characters, and limit text length to 500 characters. + return substr(str_replace(array_keys($moreformatter), array_values($moreformatter), $formatted_text), 0, 500); } }