MantisBT-Discord – Blame information for rev 1

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 <?php
2 /**
3 * Discord Integration
4 * Copyright (C) Robin van Nunen (robin@vnunen.nl) for Discord modification
5 * Copyright (C) Karim Ratib (karim@meedan.com) for original source
6 *
7 * Discord Integration is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License 2
9 * as published by the Free Software Foundation.
10 *
11 * Discord Integration is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with Discord Integration; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
19 * or see http://www.gnu.org/licenses/.
20 */
21  
22 class DiscordPlugin extends MantisPlugin
23 {
24 var $skip = false;
25  
26 function register()
27 {
28 $this->name = plugin_lang_get('title');
29 $this->description = plugin_lang_get('description');
30 $this->page = 'config_page';
31 $this->version = '1.0';
32 $this->requires = array(
33 'MantisCore' => '2.0.0',
34 );
35 $this->author = 'Robin van Nunen';
36 $this->contact = 'robin@vnunen.nl';
37 $this->url = 'https://github.com/TechGuard/MantisBT-Discord';
38 }
39  
40 function install()
41 {
42 if(version_compare(PHP_VERSION, '5.3.0', '<'))
43 {
44 plugin_error(ERROR_PHP_VERSION, ERROR);
45  
46 return false;
47 }
48 if(!extension_loaded('curl'))
49 {
50 plugin_error(ERROR_NO_CURL, ERROR);
51  
52 return false;
53 }
54  
55 return true;
56 }
57  
58 function config()
59 {
60 return array(
61 'url_webhooks' => array(),
62 'url_webhook' => '',
63 'skip_bulk' => true,
64 'link_names' => true,
65 'language' => 'english',
66 'usernames' => array(),
67 'hook_bug_report' => true,
68 'hook_bug_update' => true,
69 'hook_bug_deleted' => true,
70 'hook_bugnote_add' => true,
71 'hook_bugnote_edit' => true,
72 'hook_bugnote_deleted' => true,
73 'columns' => array(
74 'status',
75 'handler_id',
76 'priority',
77 'severity',
78 'description',
79 ),
80 );
81 }
82  
83 function hooks()
84 {
85 return array(
86 'EVENT_REPORT_BUG' => 'bug_report',
87 'EVENT_UPDATE_BUG' => 'bug_update',
88 'EVENT_BUG_DELETED' => 'bug_deleted',
89 'EVENT_BUG_ACTION' => 'bug_action',
90 'EVENT_BUGNOTE_ADD' => 'bugnote_add_edit',
91 'EVENT_BUGNOTE_EDIT' => 'bugnote_add_edit',
92 'EVENT_BUGNOTE_DELETED' => 'bugnote_deleted',
93 'EVENT_BUGNOTE_ADD_FORM' => 'bugnote_add_form',
94 );
95 }
96  
97 function bugnote_add_form($event, $bug_id)
98 {
99 if($_SERVER['PHP_SELF'] !== '/bug_update_page.php')
100 {
101 return;
102 }
103 echo '<tr>';
104 echo '<th class="category">' . plugin_lang_get('skip') . '</th>';
105 echo '<td colspan="5">';
106 echo '<label>';
107 echo '<input ', helper_get_tab_index(), ' name="slack_skip" class="ace" type="checkbox" />';
108 echo '<span class="lbl"></span>';
109 echo '</label>';
110 echo '</td></tr>';
111 }
112  
113 function bug_report_update($event, $bug, $bug_id)
114 {
115 lang_push( plugin_config_get('language') );
116 $this->skip = $this->skip || gpc_get_bool('slack_skip') || $bug->view_state == VS_PRIVATE;
117 $project = project_get_name($bug->project_id);
118 $url = string_get_bug_view_url_with_fqdn($bug_id);
119 $summary = $this->format_summary($bug);
120 $reporter = $this->get_user_name(auth_get_current_user_id());
121 $handler = $this->format_value($bug, 'handler_id');
122 $msg = sprintf(plugin_lang_get($event === 'EVENT_REPORT_BUG' ? 'bug_created' : 'bug_updated'),
123 $project, $reporter, $url, $summary, $handler
124 );
125 $this->notify($msg, $this->get_webhook($project), $this->get_attachment($bug));
126 lang_pop();
127 }
128  
129 function bug_report($event, $bug, $bug_id)
130 {
131 if(plugin_config_get('hook_bug_report', false))
132 {
133 $this->bug_report_update($event, $bug, $bug_id);
134 }
135 }
136  
137 function bug_update($event, $bug_existing, $bug_updated)
138 {
139 if(plugin_config_get('hook_bug_update', false))
140 {
141 $this->bug_report_update($event, $bug_updated, $bug_updated->id);
142 }
143 }
144  
145 function bug_action($event, $action, $bug_id)
146 {
147 $this->skip = $this->skip || gpc_get_bool('slack_skip') || plugin_config_get('skip_bulk');
148 if($action !== 'DELETE')
149 {
150 $bug = bug_get($bug_id);
151 $this->bug_update('EVENT_UPDATE_BUG', null, $bug);
152 }
153 }
154  
155 function bug_deleted($event, $bug_id)
156 {
157 if(!plugin_config_get('hook_bug_deleted', false))
158 {
159 return;
160 }
161  
162 lang_push( plugin_config_get('language') );
163 $bug = bug_get($bug_id);
164 $this->skip = $this->skip || gpc_get_bool('slack_skip') || $bug->view_state == VS_PRIVATE;
165 $project = project_get_name($bug->project_id);
166 $reporter = $this->get_user_name(auth_get_current_user_id());
167 $summary = $this->format_summary($bug);
168 $msg = sprintf(plugin_lang_get('bug_deleted'), $project, $reporter, $summary);
169 $this->notify($msg, $this->get_webhook($project));
170 lang_pop();
171 }
172  
173 function bugnote_add_edit($event, $bug_id, $bugnote_id)
174 {
175 $type = ($event === 'EVENT_BUGNOTE_ADD') ? 'add' : 'edit';
176 if(!plugin_config_get('hook_bugnote_' . $type, false))
177 {
178 return;
179 }
180  
181 lang_push( plugin_config_get('language') );
182 $bug = bug_get($bug_id);
183 $bugnote = bugnote_get($bugnote_id);
184 $this->skip = $this->skip || gpc_get_bool('slack_skip') || $bug->view_state == VS_PRIVATE || $bugnote->view_state == VS_PRIVATE;
185 $url = string_get_bugnote_view_url_with_fqdn($bug_id, $bugnote_id);
186 $project = project_get_name($bug->project_id);
187 $summary = $this->format_summary($bug);
188 $reporter = $this->get_user_name(auth_get_current_user_id());
189 $note = bugnote_get_text($bugnote_id);
190 $msg = sprintf(plugin_lang_get($event === 'EVENT_BUGNOTE_ADD' ? 'bugnote_created' : 'bugnote_updated'),
191 $project, $reporter, $url, $summary
192 );
193 $this->notify($msg, $this->get_webhook($project), $this->get_text_attachment($this->bbcode_to_slack($note)));
194 lang_pop();
195 }
196  
197 function get_text_attachment($text)
198 {
199 $attachment = array('color' => '#3AA3E3', 'mrkdwn_in' => array('pretext', 'text', 'fields'));
200 $attachment['fallback'] = "$text\n";
201 $attachment['text'] = $text;
202  
203 return $attachment;
204 }
205  
206 function bugnote_deleted($event, $bug_id, $bugnote_id)
207 {
208 if(!plugin_config_get('hook_bugnote_deleted', false))
209 {
210 return;
211 }
212  
213 lang_push( plugin_config_get('language') );
214 $bug = bug_get($bug_id);
215 $bugnote = bugnote_get($bugnote_id);
216 $this->skip = $this->skip || gpc_get_bool('slack_skip') || $bug->view_state == VS_PRIVATE || $bugnote->view_state == VS_PRIVATE;
217 $project = project_get_name($bug->project_id);
218 $url = string_get_bug_view_url_with_fqdn($bug_id);
219 $summary = $this->format_summary($bug);
220 $reporter = $this->get_user_name(auth_get_current_user_id());
221 $msg = sprintf(plugin_lang_get('bugnote_deleted'), $project, $reporter, $url, $summary);
222 $this->notify($msg, $this->get_webhook($project));
223 lang_pop();
224 }
225  
226 function format_summary($bug)
227 {
228 $summary = bug_format_id($bug->id) . ': ' . string_display_line_links($bug->summary);
229  
230 return strip_tags(html_entity_decode($summary));
231 }
232  
233 function format_text($bug, $text)
234 {
235 $t = string_display_line_links($this->bbcode_to_slack($text));
236  
237 return strip_tags(html_entity_decode($t));
238 }
239  
240 function get_attachment($bug)
241 {
242 if($bug->status == FEEDBACK)
243 {
244 $color = '#75507b';
245 }
246 else if($bug->status == ACKNOWLEDGED)
247 {
248 $color = '#f57900';
249 }
250 else if($bug->status == CONFIRMED)
251 {
252 $color = '#fce94f';
253 }
254 else if($bug->status == ASSIGNED)
255 {
256 $color = '#729fcf';
257 }
258 else if($bug->status == RESOLVED)
259 {
260 $color = '#8ae234';
261 }
262 else if($bug->status == CLOSED)
263 {
264 $color = '#8ae234';
265 }
266 else
267 {
268 $color = '#ef2929';
269 }
270  
271 $attachment = array('fallback' => '', 'color' => $color, 'mrkdwn_in' => array('pretext', 'text', 'fields'));
272 $t_columns = (array) plugin_config_get('columns');
273 foreach($t_columns as $t_column)
274 {
275 $title = column_get_title($t_column);
276 $value = $this->format_value($bug, $t_column);
277 if($title && $value)
278 {
279 $attachment['fallback'] .= $title . ': ' . $value . "\n";
280 $attachment['fields'][] = array(
281 'title' => $title,
282 'value' => $value,
283 'short' => !column_is_extended($t_column),
284 );
285 }
286 }
287  
288 return $attachment;
289 }
290  
291 function format_value($bug, $field_name) {
292 $self = $this;
293 $values = array(
294 'id' => function($bug) { return sprintf('<%s|%s>', string_get_bug_view_url_with_fqdn($bug->id), $bug->id); },
295 'project_id' => function($bug) { return project_get_name($bug->project_id); },
296 'reporter_id' => function($bug) { return $this->get_user_name($bug->reporter_id, true); },
297 'handler_id' => function($bug) { return empty($bug->handler_id) ? plugin_lang_get('no_user') : $this->get_user_name($bug->handler_id, true); },
298 'duplicate_id' => function($bug) { return sprintf('<%s|%s>', string_get_bug_view_url_with_fqdn($bug->duplicate_id), $bug->duplicate_id); },
299 'priority' => function($bug) { return get_enum_element( 'priority', $bug->priority ); },
300 'severity' => function($bug) { return get_enum_element( 'severity', $bug->severity ); },
301 'reproducibility' => function($bug) { return get_enum_element( 'reproducibility', $bug->reproducibility ); },
302 'status' => function($bug) { return get_enum_element( 'status', $bug->status ); },
303 'resolution' => function($bug) { return get_enum_element( 'resolution', $bug->resolution ); },
304 'projection' => function($bug) { return get_enum_element( 'projection', $bug->projection ); },
305 'category_id' => function($bug) { return category_full_name( $bug->category_id, false ); },
306 'eta' => function($bug) { return get_enum_element( 'eta', $bug->eta ); },
307 'view_state' => function($bug) { return $bug->view_state == VS_PRIVATE ? lang_get('private') : lang_get('public'); },
308 'sponsorship_total' => function($bug) { return sponsorship_format_amount( $bug->sponsorship_total ); },
309 'os' => function($bug) { return $bug->os; },
310 'os_build' => function($bug) { return $bug->os_build; },
311 'platform' => function($bug) { return $bug->platform; },
312 'version' => function($bug) { return $bug->version; },
313 'fixed_in_version' => function($bug) { return $bug->fixed_in_version; },
314 'target_version' => function($bug) { return $bug->target_version; },
315 'build' => function($bug) { return $bug->build; },
316 'summary' => function($bug) use($self) { return $self->format_summary($bug); },
317 'last_updated' => function($bug) { return date( config_get( 'short_date_format' ), $bug->last_updated ); },
318 'date_submitted' => function($bug) { return date( config_get( 'short_date_format' ), $bug->date_submitted ); },
319 'due_date' => function($bug) { return date( config_get( 'short_date_format' ), $bug->due_date ); },
320 'description' => function($bug) use($self) { return $self->format_text( $bug, $bug->description ); },
321 'steps_to_reproduce' => function($bug) use($self) { return $self->format_text( $bug, $bug->steps_to_reproduce ); },
322 'additional_information' => function($bug) use($self) { return $self->format_text( $bug, $bug->additional_information ); },
323 );
324 // Discover custom fields.
325 $t_related_custom_field_ids = custom_field_get_linked_ids($bug->project_id);
326 foreach($t_related_custom_field_ids as $t_id)
327 {
328 $t_def = custom_field_get_definition($t_id);
329 $values[ 'custom_' . $t_def['name'] ] = function($bug) use ($t_id)
330 {
331 return custom_field_get_value($t_id, $bug->id);
332 };
333 }
334 if(isset($values[ $field_name ]))
335 {
336 $func = $values[ $field_name ];
337  
338 return $func($bug);
339 }
340 else
341 {
342 return false;
343 }
344 }
345  
346 function get_webhook($project)
347 {
348 $webhooks = plugin_config_get('url_webhooks');
349  
350 return array_key_exists($project, $webhooks) ? $webhooks[ $project ] : plugin_config_get('url_webhook');
351 }
352  
353 function notify($msg, $webhook, $attachment = false)
354 {
355 if($this->skip)
356 {
357 return;
358 }
359 if(empty($webhook))
360 {
361 return;
362 }
363 $url = sprintf('%s', trim($webhook));
364 if(substr($url, -strlen('/slack')) != '/slack')
365 {
366 $url .= '/slack';
367 }
368  
369 $ch = curl_init();
370 curl_setopt($ch, CURLOPT_URL, $url);
371 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
372 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
373 $payload = array(
374 'text' => $msg,
375 );
376 if($attachment)
377 {
378 $payload['attachments'] = array($attachment);
379 }
380 $data = array('payload' => json_encode($payload));
381 curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
382 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
383 curl_setopt($ch, CURLOPT_TIMEOUT, 5);
384 $result = curl_exec($ch);
385 if($result !== 'ok')
386 {
387 trigger_error(curl_errno($ch) . ': ' . curl_error($ch), E_USER_WARNING);
388 plugin_error('ERROR_CURL', E_USER_ERROR);
389 }
390 curl_close($ch);
391 }
392  
393 function bbcode_to_slack($bbtext)
394 {
395 $bbtags = array(
396 '[b]' => '*','[/b]' => '* ',
397 '[i]' => '_','[/i]' => '_ ',
398 '[u]' => '_','[/u]' => '_ ',
399 '[s]' => '~','[/s]' => '~ ',
400 '[sup]' => '','[/sup]' => '',
401 '[sub]' => '','[/sub]' => '',
402  
403 '[list]' => '','[/list]' => "\n",
404 '[*]' => 'ā€¢ ',
405  
406 '[hr]' => "\nā€”ā€”ā€”\n",
407  
408 '[left]' => '','[/left]' => '',
409 '[right]' => '','[/right]' => '',
410 '[center]' => '','[/center]' => '',
411 '[justify]' => '','[/justify]' => '',
412 );
413 $bbtext = str_ireplace(array_keys($bbtags), array_values($bbtags), $bbtext);
414 $bbextended = array(
415 "/\[code(.*?)\](.*?)\[\/code\]/is" => "```$2```",
416 "/\[color(.*?)\](.*?)\[\/color\]/is" => "$2",
417 "/\[size=(.*?)\](.*?)\[\/size\]/is" => "$2",
418 "/\[highlight(.*?)\](.*?)\[\/highlight\]/is" => "$2",
419 "/\[url](.*?)\[\/url]/i" => "<$1>",
420 "/\[url=(.*?)\](.*?)\[\/url\]/i" => "<$1|$2>",
421 "/\[email=(.*?)\](.*?)\[\/email\]/i" => "<mailto:$1|$2>",
422 "/\[img\]([^[]*)\[\/img\]/i" => "<$1>",
423 );
424 foreach($bbextended as $match => $replacement)
425 {
426 $bbtext = preg_replace($match, $replacement, $bbtext);
427 }
428 $bbtext = preg_replace_callback("/\[quote(=)?(.*?)\](.*?)\[\/quote\]/is",
429 function($matches)
430 {
431 if(!empty($matches[2]))
432 {
433 $result = "\n> _*" . $matches[2] . "* wrote:_\n> \n";
434 }
435 $lines = explode("\n", $matches[3]);
436 foreach($lines as $line)
437 {
438 $result .= "> " . $line . "\n";
439 }
440  
441 return $result;
442 }, $bbtext);
443  
444 return $bbtext;
445 }
446  
447 function get_user_name($user_id, $discord = false)
448 {
449 $user = user_get_row($user_id);
450 $username = $user['username'];
451 if(!$discord || !plugin_config_get('link_names'))
452 {
453 return $username;
454 }
455 $usernames = plugin_config_get('usernames');
456 if(array_key_exists($username, $usernames))
457 {
458 return '<@' . $usernames[$username] . '>';
459 }
460 return $username;
461 }
462  
463 }