├── LICENSE ├── README.md ├── config.php ├── plugin.php └── slack.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 thammanna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Slack](https://a.slack-edge.com/ae57/img/slack_api_logo.png) 2 | 3 | osTicket-slack 4 | ============== 5 | An plugin for [osTicket](https://osticket.com) which posts notifications to a [Slack](https://slack.com) channel. 6 | 7 | Originally forked from: [https://github.com/thammanna/osticket-slack](https://github.com/thammanna/osticket-slack). 8 | 9 | Info 10 | ------ 11 | This plugin uses CURL and was designed/tested with osTicket-1.10.1 12 | 13 | ## Requirements 14 | - php_curl 15 | - A slack account 16 | 17 | ## Install 18 | -------- 19 | 1. Clone this repo or download the zip file and place the contents into your `include/plugins` folder. 20 | 1. Now the plugin needs to be enabled & configured, so login to osTicket, select "Admin Panel" then "Manage -> Plugins" you should be seeing the list of currently installed plugins. 21 | 1. Click on `Slack Notifier` and paste your Slack Endpoint URL into the box (Slack setup instructions below). 22 | 1. Click `Save Changes`! (If you get an error about curl, you will need to install the Curl module for PHP). 23 | 1. After that, go back to the list of plugins and tick the checkbox next to "Slack Notifier" and select the "Enable" button. 24 | 25 | 26 | ## Slack Setup: 27 | - Navigate to https://api.slack.com/ select "Start Building" 28 | - Name your App `osTicket Notification`, select your Workspace from the drop-down 29 | - Select "Incoming Webhooks" 30 | - Activate the Webhooks with the link (it defaults to Off, just click Off to change it to On) 31 | - Scroll to the bottom and select "Add a new Webhook to Workspace" 32 | - Select the endpoint of the webhook, (ie, channel to post to) 33 | - Select "Authorize" 34 | - Scroll down and copy the Webhook URL entirely, paste this into the `osTicket -> Admin -> Plugin -> Slack` config admin screen. 35 | 36 | If you want to add the Department as a field in each slack notice, tick the Checkbox in the Plugin config. 37 | 38 | The channel you select will receive an event notice, like: 39 | ``` 40 | Aaron [10:56 AM] added an integration to this channel: osTicket Notification 41 | ``` 42 | You should also receive an email from Slack telling you about the new Integration. 43 | 44 | 45 | ## Discord Setup: 46 | Note: This works very well, but may not be as smooth as Slack is natively. 47 | 48 | - Open Discord 49 | - Right-click on the channel you wish to send the notifications too 50 | - Select "Webhooks" 51 | - Create a Webhook by clicking 'Create Webhook' 52 | - Scroll down to the bottom and copy the Webhook URL in it's entireity 53 | - Go to the `osTicket -> Admin -> Plugin -> Slack` config admin screen and paste the URL, at the end add `/slack` 54 | - Example: https://discordapp.com/api/webhooks/{webhook.id}/{webhook.token}/slack 55 | 56 | ## Test! 57 | Create a ticket! 58 | 59 | You should see something like the following appear in your Slack channel: 60 | 61 | ![slack-new-ticket](https://user-images.githubusercontent.com/5077391/31572647-923e07b0-b0f6-11e7-9515-98205d6f800f.png) 62 | 63 | When a user replies, you'll get something like: 64 | 65 | ![slack-reply](https://user-images.githubusercontent.com/5077391/31572648-9279eb18-b0f6-11e7-97da-9a9c63a200d4.png) 66 | 67 | Notes, Replies from Agents and System messages shouldn't appear, usernames are links to the user's page 68 | in osTicket, the Ticket subject is a link to the ticket, as is the ticket ID. 69 | 70 | ## Adding pull's from original repo: 71 | +0.2 - 17 december 2016 72 | +[feature] "Ignore when subject equals regex" by @ramonfincken 73 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | new SectionBreakField(array( 36 | 'label' => $__('Slack notifier'), 37 | 'hint' => $__('Readme first: https://github.com/clonemeagain/osticket-slack') 38 | )), 39 | 'slack-webhook-url' => new TextboxField(array( 40 | 'label' => $__('Webhook URL'), 41 | 'configuration' => array( 42 | 'size' => 100, 43 | 'length' => 200 44 | ), 45 | )), 46 | 'slack-regex-subject-ignore' => new TextboxField([ 47 | 'label' => $__('Ignore when subject equals regex'), 48 | 'hint' => $__('Auto delimited, always case-insensitive'), 49 | 'configuration' => [ 50 | 'size' => 30, 51 | 'length' => 200 52 | ], 53 | ]), 54 | 'slack-update-types' => new ChoiceField([ 55 | 'label' => $__('Update Types'), 56 | 'hint' => $__('What types of updates should be sent via Slack?'), 57 | 'choices' => array('both' => 'New & Updated Tickets', 'updatesOnly' => 'Only Ticket Updates', 'newOnly' => 'Only New Tickets'), 58 | 'default' => 'both', 59 | 'configuration' => [ 60 | 'size' => 30, 61 | 'length' => 200 62 | ], 63 | ]), 64 | 'message-template' => new TextareaField([ 65 | 'label' => $__('Message Template'), 66 | 'hint' => $__('The main text part of the Slack message, uses Ticket Variables, for what the user typed, use variable: %{slack_safe_message}'), 67 | // "<%{url}/scp/tickets.php?id=%{ticket.id}|%{ticket.subject}>\n" // Already included as Title 68 | 'default' => "%{ticket.name.full} (%{ticket.email}) in *%{ticket.dept}* _%{ticket.topic}_\n\n```%{slack_safe_message}```", 69 | 'configuration' => [ 70 | 'html' => FALSE, 71 | ] 72 | ]) 73 | ); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /plugin.php: -------------------------------------------------------------------------------- 1 | 'osticket:slack', 5 | 'version' => '0.2', 6 | 'name' => 'Slack notifier', 7 | 'author' => 'Thammanna Jammada', 8 | 'description' => 'Notify Slack on new ticket.', 9 | 'url' => 'https://github.com/thammanna/osticket-slack', 10 | 'plugin' => 'slack.php:SlackPlugin', 11 | ); 12 | -------------------------------------------------------------------------------- /slack.php: -------------------------------------------------------------------------------- 1 | getInstance($id))) 19 | return $i; 20 | 21 | return $this->getInstances()->first(); 22 | } 23 | 24 | /** 25 | * The entrypoint of the plugin, keep short, always runs. 26 | */ 27 | function bootstrap() { 28 | // get plugin instances 29 | self::$pluginInstance = self::getPluginInstance(null); 30 | 31 | $updateTypes = $this->getConfig(self::$pluginInstance)->get('slack-update-types'); 32 | 33 | // Listen for osTicket to tell us it's made a new ticket or updated 34 | // an existing ticket: 35 | if($updateTypes == 'both' || $updateTypes == 'newOnly' || empty($updateTypes)) { 36 | Signal::connect('ticket.created', array($this, 'onTicketCreated')); 37 | } 38 | 39 | if($updateTypes == 'both' || $updateTypes == 'updatesOnly' || empty($updateTypes)) { 40 | Signal::connect('threadentry.created', array($this, 'onTicketUpdated')); 41 | } 42 | // Tasks? Signal::connect('task.created',array($this,'onTaskCreated')); 43 | } 44 | 45 | /** 46 | * What to do with a new Ticket? 47 | * 48 | * @global OsticketConfig $cfg 49 | * @param Ticket $ticket 50 | * @return type 51 | */ 52 | function onTicketCreated(Ticket $ticket) { 53 | global $cfg; 54 | if (!$cfg instanceof OsticketConfig) { 55 | error_log("Slack plugin called too early."); 56 | return; 57 | } 58 | 59 | // if slack-update-types is "updatesOnly", then don't send this! 60 | if($this->getConfig(self::$pluginInstance)->get('slack-update-types') == 'updatesOnly') {return;} 61 | 62 | // Convert any HTML in the message into text 63 | $plaintext = Format::html2text($ticket->getMessages()[0]->getBody()->getClean()); 64 | 65 | // Format the messages we'll send. 66 | $heading = sprintf('%s CONTROLSTART%sscp/tickets.php?id=%d|#%sCONTROLEND %s' 67 | , __("New Ticket") 68 | , $cfg->getUrl() 69 | , $ticket->getId() 70 | , $ticket->getNumber() 71 | , __("created")); 72 | $this->sendToSlack($ticket, $heading, $plaintext); 73 | } 74 | 75 | /** 76 | * What to do with an Updated Ticket? 77 | * 78 | * @global OsticketConfig $cfg 79 | * @param ThreadEntry $entry 80 | * @return type 81 | */ 82 | function onTicketUpdated(ThreadEntry $entry) { 83 | global $cfg; 84 | if (!$cfg instanceof OsticketConfig) { 85 | error_log("Slack plugin called too early."); 86 | return; 87 | } 88 | 89 | // if slack-update-types is "newOnly", then don't send this! 90 | if($this->getConfig(self::$pluginInstance)->get('slack-update-types') == 'newOnly') {return;} 91 | 92 | if (!$entry instanceof MessageThreadEntry) { 93 | // this was a reply or a system entry.. not a message from a user 94 | return; 95 | } 96 | 97 | // Need to fetch the ticket from the ThreadEntry 98 | $ticket = $this->getTicket($entry); 99 | if (!$ticket instanceof Ticket) { 100 | // Admin created ticket's won't work here. 101 | return; 102 | } 103 | 104 | // Check to make sure this entry isn't the first (ie: a New ticket) 105 | $first_entry = $ticket->getMessages()[0]; 106 | if ($entry->getId() == $first_entry->getId()) { 107 | return; 108 | } 109 | // Convert any HTML in the message into text 110 | $plaintext = Format::html2text($entry->getBody()->getClean()); 111 | 112 | // Format the messages we'll send 113 | $heading = sprintf('%s CONTROLSTART%sscp/tickets.php?id=%d|#%sCONTROLEND %s' 114 | , __("Ticket") 115 | , $cfg->getUrl() 116 | , $ticket->getId() 117 | , $ticket->getNumber() 118 | , __("updated")); 119 | $this->sendToSlack($ticket, $heading, $plaintext, 'warning'); 120 | } 121 | 122 | /** 123 | * A helper function that sends messages to slack endpoints. 124 | * 125 | * @global osTicket $ost 126 | * @global OsticketConfig $cfg 127 | * @param Ticket $ticket 128 | * @param string $heading 129 | * @param string $body 130 | * @param string $colour 131 | * @throws \Exception 132 | */ 133 | function sendToSlack(Ticket $ticket, $heading, $body, $colour = 'good') { 134 | global $ost, $cfg; 135 | if (!$ost instanceof osTicket || !$cfg instanceof OsticketConfig) { 136 | error_log("Slack plugin called too early."); 137 | return; 138 | } 139 | $url = $this->getConfig(self::$pluginInstance)->get('slack-webhook-url'); 140 | if (!$url) { 141 | $ost->logError('Slack Plugin not configured', 'You need to read the Readme and configure a webhook URL before using this.'); 142 | } 143 | 144 | // Check the subject, see if we want to filter it. 145 | $regex_subject_ignore = $this->getConfig(self::$pluginInstance)->get('slack-regex-subject-ignore'); 146 | // Filter on subject, and validate regex: 147 | if ($regex_subject_ignore && preg_match("/$regex_subject_ignore/i", $ticket->getSubject())) { 148 | $ost->logDebug('Ignored Message', 'Slack notification was not sent because the subject (' . $ticket->getSubject() . ') matched regex (' . htmlspecialchars($regex_subject_ignore) . ').'); 149 | return; 150 | } 151 | 152 | $heading = $this->format_text($heading); 153 | 154 | // Pull template from config, and use that. 155 | $template = $this->getConfig(self::$pluginInstance)->get('message-template'); 156 | // Add our custom var 157 | $custom_vars = [ 158 | 'slack_safe_message' => $this->format_text($body), 159 | ]; 160 | $formatted_message = $ticket->replaceVars($template, $custom_vars); 161 | 162 | // Build the payload with the formatted data: 163 | $payload['attachments'][0] = [ 164 | 'pretext' => $heading, 165 | 'fallback' => $heading, 166 | 'color' => $colour, 167 | // 'author' => $ticket->getOwner(), 168 | // 'author_link' => $cfg->getUrl() . 'scp/users.php?id=' . $ticket->getOwnerId(), 169 | // 'author_icon' => $this->get_gravatar($ticket->getEmail()), 170 | 'title' => $ticket->getSubject(), 171 | 'title_link' => $cfg->getUrl() . 'scp/tickets.php?id=' . $ticket->getId(), 172 | 'ts' => time(), 173 | 'footer' => 'via osTicket Slack Plugin', 174 | 'footer_icon' => 'https://platform.slack-edge.com/img/default_application_icon.png', 175 | 'text' => $formatted_message, 176 | 'mrkdwn_in' => ["text"] 177 | ]; 178 | // Add a field for tasks if there are open ones 179 | if ($ticket->getNumOpenTasks()) { 180 | $payload['attachments'][0]['fields'][] = [ 181 | 'title' => __('Open Tasks'), 182 | 'value' => $ticket->getNumOpenTasks(), 183 | 'short' => TRUE, 184 | ]; 185 | } 186 | // Change the colour to Fuschia if ticket is overdue 187 | if ($ticket->isOverdue()) { 188 | $payload['attachments'][0]['colour'] = '#ff00ff'; 189 | } 190 | 191 | // Format the payload: 192 | $data_string = utf8_encode(json_encode($payload)); 193 | 194 | try { 195 | // Setup curl 196 | $ch = curl_init($url); 197 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); 198 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string); 199 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 200 | curl_setopt($ch, CURLOPT_HTTPHEADER, array( 201 | 'Content-Type: application/json', 202 | 'Content-Length: ' . strlen($data_string)) 203 | ); 204 | 205 | // Actually send the payload to slack: 206 | if (curl_exec($ch) === false) { 207 | throw new \Exception($url . ' - ' . curl_error($ch)); 208 | } else { 209 | $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 210 | if ($statusCode != '200') { 211 | throw new \Exception( 212 | 'Error sending to: ' . $url 213 | . ' Http code: ' . $statusCode 214 | . ' curl-error: ' . curl_errno($ch)); 215 | } 216 | } 217 | } catch (\Exception $e) { 218 | $ost->logError('Slack posting issue!', $e->getMessage(), true); 219 | error_log('Error posting to Slack. ' . $e->getMessage()); 220 | } finally { 221 | curl_close($ch); 222 | } 223 | } 224 | 225 | /** 226 | * Fetches a ticket from a ThreadEntry 227 | * 228 | * @param ThreadEntry $entry 229 | * @return Ticket 230 | */ 231 | function getTicket(ThreadEntry $entry) { 232 | $ticket_id = Thread::objects()->filter([ 233 | 'id' => $entry->getThreadId() 234 | ])->values_flat('object_id')->first() [0]; 235 | 236 | // Force lookup rather than use cached data.. 237 | // This ensures we get the full ticket, with all 238 | // thread entries etc.. 239 | return Ticket::lookup(array( 240 | 'ticket_id' => $ticket_id 241 | )); 242 | } 243 | 244 | /** 245 | * Formats text according to the 246 | * formatting rules:https://api.slack.com/docs/message-formatting 247 | * 248 | * @param string $text 249 | * @return string 250 | */ 251 | function format_text($text) { 252 | $formatter = [ 253 | '<' => '<', 254 | '>' => '>', 255 | '&' => '&' 256 | ]; 257 | $formatted_text = str_replace(array_keys($formatter), array_values($formatter), $text); 258 | // put the <>'s control characters back in 259 | $moreformatter = [ 260 | 'CONTROLSTART' => '<', 261 | 'CONTROLEND' => '>' 262 | ]; 263 | // Replace the CONTROL characters, and limit text length to 500 characters. 264 | return mb_substr(str_replace(array_keys($moreformatter), array_values($moreformatter), $formatted_text), 0, 500); 265 | } 266 | 267 | /** 268 | * Get either a Gravatar URL or complete image tag for a specified email address. 269 | * 270 | * @param string $email The email address 271 | * @param string $s Size in pixels, defaults to 80px [ 1 - 2048 ] 272 | * @param string $d Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ] 273 | * @param string $r Maximum rating (inclusive) [ g | pg | r | x ] 274 | * @param boole $img True to return a complete IMG tag False for just the URL 275 | * @param array $atts Optional, additional key/value attributes to include in the IMG tag 276 | * @return String containing either just a URL or a complete image tag 277 | * @source https://gravatar.com/site/implement/images/php/ 278 | */ 279 | function get_gravatar($email, $s = 80, $d = 'mm', $r = 'g', $img = false, $atts = array()) { 280 | $url = 'https://www.gravatar.com/avatar/'; 281 | $url .= md5(strtolower(trim($email))); 282 | $url .= "?s=$s&d=$d&r=$r"; 283 | if ($img) { 284 | $url = ' $val) 286 | $url .= ' ' . $key . '="' . $val . '"'; 287 | $url .= ' />'; 288 | } 289 | return $url; 290 | } 291 | 292 | } 293 | --------------------------------------------------------------------------------