MantisBT-Discord – Blame information for rev 3

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