-
Notifications
You must be signed in to change notification settings - Fork 34
/
slack.php
292 lines (259 loc) · 10.9 KB
/
slack.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
<?php
require_once(INCLUDE_DIR . 'class.signal.php');
require_once(INCLUDE_DIR . 'class.plugin.php');
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";
static $pluginInstance = null;
private function getPluginInstance(?int $id) {
if($id && ($i = $this->getInstance($id)))
return $i;
return $this->getInstances()->first();
}
/**
* The entrypoint of the plugin, keep short, always runs.
*/
function bootstrap() {
// get plugin instances
self::$pluginInstance = self::getPluginInstance(null);
$updateTypes = $this->getConfig(self::$pluginInstance)->get('slack-update-types');
// Listen for osTicket to tell us it's made a new ticket or updated
// an existing ticket:
if($updateTypes == 'both' || $updateTypes == 'newOnly' || empty($updateTypes)) {
Signal::connect('ticket.created', array($this, 'onTicketCreated'));
}
if($updateTypes == 'both' || $updateTypes == 'updatesOnly' || empty($updateTypes)) {
Signal::connect('threadentry.created', array($this, 'onTicketUpdated'));
}
// Tasks? Signal::connect('task.created',array($this,'onTaskCreated'));
}
/**
* 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) {
error_log("Slack plugin called too early.");
return;
}
// if slack-update-types is "updatesOnly", then don't send this!
if($this->getConfig(self::$pluginInstance)->get('slack-update-types') == 'updatesOnly') {return;}
// 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|#%sCONTROLEND %s'
, __("New Ticket")
, $cfg->getUrl()
, $ticket->getId()
, $ticket->getNumber()
, __("created"));
$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) {
error_log("Slack plugin called too early.");
return;
}
// if slack-update-types is "newOnly", then don't send this!
if($this->getConfig(self::$pluginInstance)->get('slack-update-types') == 'newOnly') {return;}
if (!$entry instanceof MessageThreadEntry) {
// this was a reply or a system entry.. not a message from a user
return;
}
// Need to fetch the ticket from the ThreadEntry
$ticket = $this->getTicket($entry);
if (!$ticket instanceof Ticket) {
// Admin created ticket's won't work here.
return;
}
// 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()) {
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|#%sCONTROLEND %s'
, __("Ticket")
, $cfg->getUrl()
, $ticket->getId()
, $ticket->getNumber()
, __("updated"));
$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(self::$pluginInstance)->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.');
}
// Check the subject, see if we want to filter it.
$regex_subject_ignore = $this->getConfig(self::$pluginInstance)->get('slack-regex-subject-ignore');
// Filter on subject, and validate regex:
if ($regex_subject_ignore && preg_match("/$regex_subject_ignore/i", $ticket->getSubject())) {
$ost->logDebug('Ignored Message', 'Slack notification was not sent because the subject (' . $ticket->getSubject() . ') matched regex (' . htmlspecialchars($regex_subject_ignore) . ').');
return;
}
$heading = $this->format_text($heading);
// Pull template from config, and use that.
$template = $this->getConfig(self::$pluginInstance)->get('message-template');
// Add our custom var
$custom_vars = [
'slack_safe_message' => $this->format_text($body),
];
$formatted_message = $ticket->replaceVars($template, $custom_vars);
// Build the payload with the formatted data:
$payload['attachments'][0] = [
'pretext' => $heading,
'fallback' => $heading,
'color' => $colour,
// 'author' => $ticket->getOwner(),
// 'author_link' => $cfg->getUrl() . 'scp/users.php?id=' . $ticket->getOwnerId(),
// 'author_icon' => $this->get_gravatar($ticket->getEmail()),
'title' => $ticket->getSubject(),
'title_link' => $cfg->getUrl() . 'scp/tickets.php?id=' . $ticket->getId(),
'ts' => time(),
'footer' => 'via osTicket Slack Plugin',
'footer_icon' => 'https://platform.slack-edge.com/img/default_application_icon.png',
'text' => $formatted_message,
'mrkdwn_in' => ["text"]
];
// 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';
}
// 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);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'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(
'Error sending to: ' . $url
. ' Http code: ' . $statusCode
. ' curl-error: ' . curl_errno($ch));
}
}
} catch (\Exception $e) {
$ost->logError('Slack posting issue!', $e->getMessage(), true);
error_log('Error posting to Slack. ' . $e->getMessage());
} finally {
curl_close($ch);
}
}
/**
* Fetches a ticket from a ThreadEntry
*
* @param ThreadEntry $entry
* @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 mb_substr(str_replace(array_keys($moreformatter), array_values($moreformatter), $formatted_text), 0, 500);
}
/**
* Get either a Gravatar URL or complete image tag for a specified email address.
*
* @param string $email The email address
* @param string $s Size in pixels, defaults to 80px [ 1 - 2048 ]
* @param string $d Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ]
* @param string $r Maximum rating (inclusive) [ g | pg | r | x ]
* @param boole $img True to return a complete IMG tag False for just the URL
* @param array $atts Optional, additional key/value attributes to include in the IMG tag
* @return String containing either just a URL or a complete image tag
* @source https://gravatar.com/site/implement/images/php/
*/
function get_gravatar($email, $s = 80, $d = 'mm', $r = 'g', $img = false, $atts = array()) {
$url = 'https://www.gravatar.com/avatar/';
$url .= md5(strtolower(trim($email)));
$url .= "?s=$s&d=$d&r=$r";
if ($img) {
$url = '<img src="' . $url . '"';
foreach ($atts as $key => $val)
$url .= ' ' . $key . '="' . $val . '"';
$url .= ' />';
}
return $url;
}
}