diff options
author | Frederic Guillot <fred@kanboard.net> | 2015-03-28 21:37:53 -0400 |
---|---|---|
committer | Frederic Guillot <fred@kanboard.net> | 2015-03-28 21:37:53 -0400 |
commit | 5536f6c6ce591ba05a169d2e33b6fb240378d8a4 (patch) | |
tree | bac0cb1563d8258965384b97be8d425578f71ef6 | |
parent | f9891a966fb87d2112f174b7c3a1b3a705b73bdd (diff) |
Add Slack integration
32 files changed, 296 insertions, 47 deletions
diff --git a/README.markdown b/README.markdown index 52667d86..fd34c6fb 100644 --- a/README.markdown +++ b/README.markdown @@ -97,6 +97,7 @@ Documentation - [Bitbucket webhooks](docs/bitbucket-webhooks.markdown) - [Github webhooks](docs/github-webhooks.markdown) - [Gitlab webhooks](docs/gitlab-webhooks.markdown) +- [Slack](docs/slack.markdown) #### More diff --git a/app/Controller/Config.php b/app/Controller/Config.php index 6f3bc43c..57f586ae 100644 --- a/app/Controller/Config.php +++ b/app/Controller/Config.php @@ -43,6 +43,9 @@ class Config extends Base if ($redirect === 'board') { $values += array('subtask_restriction' => 0, 'subtask_time_tracking' => 0, 'subtask_forecast' => 0); } + else if ($redirect === 'integrations') { + $values += array('integration_slack_webhook' => 0); + } if ($this->config->save($values)) { $this->config->reload(); @@ -102,6 +105,20 @@ class Config extends Base } /** + * Display the integration settings page + * + * @access public + */ + public function integrations() + { + $this->common('integrations'); + + $this->response->html($this->layout('config/integrations', array( + 'title' => t('Settings').' > '.t('Integrations'), + ))); + } + + /** * Display the webhook settings page * * @access public diff --git a/app/Core/HttpClient.php b/app/Core/HttpClient.php new file mode 100644 index 00000000..96860152 --- /dev/null +++ b/app/Core/HttpClient.php @@ -0,0 +1,67 @@ +<?php + +namespace Core; + +/** + * HTTP client + * + * @package core + * @author Frederic Guillot + */ +class HttpClient +{ + /** + * HTTP connection timeout in seconds + * + * @var integer + */ + const HTTP_TIMEOUT = 2; + + /** + * Number of maximum redirections for the HTTP client + * + * @var integer + */ + const HTTP_MAX_REDIRECTS = 2; + + /** + * HTTP client user agent + * + * @var string + */ + const HTTP_USER_AGENT = 'Kanboard Webhook'; + + /** + * Send a POST HTTP request + * + * @static + * @access public + * @param string $url + * @param array $data + * @return string + */ + public static function post($url, array $data) + { + if (empty($url)) { + return ''; + } + + $headers = array( + 'Connection: close', + 'User-Agent: '.self::HTTP_USER_AGENT, + ); + + $context = stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'protocol_version' => 1.1, + 'timeout' => self::HTTP_TIMEOUT, + 'max_redirects' => self::HTTP_MAX_REDIRECTS, + 'header' => implode("\r\n", $headers), + 'content' => json_encode($data) + ) + )); + + return @file_get_contents(trim($url), false, $context); + } +} diff --git a/app/Integration/SlackWebhook.php b/app/Integration/SlackWebhook.php new file mode 100644 index 00000000..fc7daeb4 --- /dev/null +++ b/app/Integration/SlackWebhook.php @@ -0,0 +1,43 @@ +<?php + +namespace Integration; + +/** + * Slack Webhook + * + * @package integration + * @author Frederic Guillot + */ +class SlackWebhook extends Base +{ + /** + * Send message to the incoming Slack webhook + * + * @access public + * @param integer $project_id Project id + * @param integer $task_id Task id + * @param string $event_name Event name + * @param array $data Event data + */ + public function notify($project_id, $task_id, $event_name, array $event) + { + $project = $this->project->getbyId($project_id); + + $event['event_name'] = $event_name; + $event['author'] = $this->user->getFullname($this->session['user']); + + $payload = array( + 'text' => '*['.$project['name'].']* '.str_replace('"', '"', $this->projectActivity->getTitle($event)), + 'username' => 'Kanboard', + 'icon_url' => 'http://kanboard.net/assets/img/favicon.png', + ); + + if ($this->config->get('application_url')) { + $payload['text'] .= ' - <'.$this->config->get('application_url'); + $payload['text'] .= $this->helper->u('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id)); + $payload['text'] .= '|'.t('view the task on Kanboard').'>'; + } + + $this->httpClient->post($this->config->get('integration_slack_webhook_url'), $payload); + } +} diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index e11f01ce..9af5dcb8 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index ac7fb8ee..dda991c7 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index 6abd3e77..a857493f 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index 39b85832..ae238224 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index fec0f8b8..e2425892 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -825,4 +825,8 @@ return array( 'Reference currency' => 'Devise de référence', 'The currency rate have been added successfully.' => 'Le taux de change a été ajouté avec succès.', 'Unable to add this currency rate.' => 'Impossible d\'ajouter ce taux de change', + 'Send notifications to a Slack channel' => 'Envoyer des notifications sur un channel Slack', + 'Webhook URL' => 'URL du webhook', + 'Help on Slack integration' => 'Aide sur l\'intégration avec Slack', + '%s remove the assignee of the task %s' => '%s a enlevé la personne assignée à la tâche %s', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index 8feab08e..70ab8898 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 3c059854..5924ed0c 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index 2b9a1e45..f7d225af 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index 839b0155..0efbc7f3 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index 2a04375a..97c185c3 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 79bda55b..bca27142 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index 23f4dd11..8b84fc1f 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index 594abb31..a04a31af 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index 0bcecdb4..b5e6a629 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index 5a52c9e1..f45b24cb 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index b556595e..d46e71a4 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index eb0f5126..b6be113d 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -823,4 +823,8 @@ return array( // 'Reference currency' => '', // 'The currency rate have been added successfully.' => '', // 'Unable to add this currency rate.' => '', + // 'Send notifications to a Slack channel' => '', + // 'Webhook URL' => '', + // 'Help on Slack integration' => '', + // '%s remove the assignee of the task %s' => '', ); diff --git a/app/Model/ProjectActivity.php b/app/Model/ProjectActivity.php index 652cc842..c5fbbd38 100644 --- a/app/Model/ProjectActivity.php +++ b/app/Model/ProjectActivity.php @@ -162,7 +162,13 @@ class ProjectActivity extends Base { switch ($event['event_name']) { case Task::EVENT_ASSIGNEE_CHANGE: - return t('%s change the assignee of the task #%d to %s', $event['author'], $event['task']['id'], $event['task']['assignee_name'] ?: $event['task']['assignee_username']); + $assignee = $event['task']['assignee_name'] ?: $event['task']['assignee_username']; + + if (! empty($assignee)) { + return t('%s change the assignee of the task #%d to %s', $event['author'], $event['task']['id'], $assignee); + } + + return t('%s remove the assignee of the task %s', $event['author'], e('#%d', $event['task']['id'])); case Task::EVENT_UPDATE: return t('%s updated the task #%d', $event['author'], $event['task']['id']); case Task::EVENT_CREATE: diff --git a/app/Model/Webhook.php b/app/Model/Webhook.php index 7edffa6e..b3603818 100644 --- a/app/Model/Webhook.php +++ b/app/Model/Webhook.php @@ -11,27 +11,6 @@ namespace Model; class Webhook extends Base { /** - * HTTP connection timeout in seconds - * - * @var integer - */ - const HTTP_TIMEOUT = 1; - - /** - * Number of maximum redirections for the HTTP client - * - * @var integer - */ - const HTTP_MAX_REDIRECTS = 3; - - /** - * HTTP client user agent - * - * @var string - */ - const HTTP_USER_AGENT = 'Kanboard Webhook'; - - /** * Call the external URL * * @access public @@ -42,22 +21,6 @@ class Webhook extends Base { $token = $this->config->get('webhook_token'); - $headers = array( - 'Connection: close', - 'User-Agent: '.self::HTTP_USER_AGENT, - ); - - $context = stream_context_create(array( - 'http' => array( - 'method' => 'POST', - 'protocol_version' => 1.1, - 'timeout' => self::HTTP_TIMEOUT, - 'max_redirects' => self::HTTP_MAX_REDIRECTS, - 'header' => implode("\r\n", $headers), - 'content' => json_encode($task) - ) - )); - if (strpos($url, '?') !== false) { $url .= '&token='.$token; } @@ -65,6 +28,6 @@ class Webhook extends Base $url .= '?token='.$token; } - @file_get_contents($url, false, $context); + return $this->httpClient->post($url, $task); } } diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index bac65ec4..dc0b8b42 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,14 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 57; +const VERSION = 58; + +function version_58($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_slack_webhook', '0')); + $rq->execute(array('integration_slack_webhook_url', '')); +} function version_57($pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index dc4afff3..ea7b84d1 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,14 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 38; +const VERSION = 39; + +function version_39($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_slack_webhook', '0')); + $rq->execute(array('integration_slack_webhook_url', '')); +} function version_38($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index 3e3366a4..1ffd9405 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,14 @@ use Core\Security; use PDO; use Model\Link; -const VERSION = 56; +const VERSION = 57; + +function version_57($pdo) +{ + $rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)'); + $rq->execute(array('integration_slack_webhook', '0')); + $rq->execute(array('integration_slack_webhook_url', '')); +} function version_56($pdo) { diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index 9ee4c18f..5f6298aa 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -69,11 +69,13 @@ class ClassProvider implements ServiceProviderInterface 'MemoryCache', 'FileCache', 'Request', + 'HttpClient', ), 'Integration' => array( 'GitlabWebhook', 'GithubWebhook', 'BitbucketWebhook', + 'SlackWebhook', ) ); diff --git a/app/Subscriber/ProjectActivitySubscriber.php b/app/Subscriber/ProjectActivitySubscriber.php index 00f5b044..d2e85166 100644 --- a/app/Subscriber/ProjectActivitySubscriber.php +++ b/app/Subscriber/ProjectActivitySubscriber.php @@ -41,6 +41,15 @@ class ProjectActivitySubscriber extends Base implements EventSubscriberInterface $event_name, $values ); + + if ($this->config->get('integration_slack_webhook') == 1) { + $this->slackWebhook->notify( + $values['task']['project_id'], + $values['task']['id'], + $event_name, + $values + ); + } } } diff --git a/app/Template/config/integrations.php b/app/Template/config/integrations.php new file mode 100644 index 00000000..104ebc16 --- /dev/null +++ b/app/Template/config/integrations.php @@ -0,0 +1,22 @@ +<div class="page-header"> + <h2><?= t('Integration with third-party services') ?></h2> +</div> + +<form method="post" action="<?= $this->u('config', 'integrations') ?>" autocomplete="off"> + + <?= $this->formCsrf() ?> + + <h3><i class="fa fa-slack fa-fw"></i> <?= t('Slack') ?></h3> + <div class="listing"> + <?= $this->formCheckbox('integration_slack_webhook', t('Send notifications to a Slack channel'), 1, $values['integration_slack_webhook'] == 1) ?> + + <?= $this->formLabel(t('Webhook URL'), 'integration_slack_webhook_url') ?> + <?= $this->formText('integration_slack_webhook_url', $values, $errors) ?> + + <p class="form-help"><a href="http://kanboard.net/documentation/slack" target="_blank"><?= t('Help on Slack integration') ?></a></p> + </div> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> +</form>
\ No newline at end of file diff --git a/app/Template/config/sidebar.php b/app/Template/config/sidebar.php index 2e81d48a..a4f9d8e3 100644 --- a/app/Template/config/sidebar.php +++ b/app/Template/config/sidebar.php @@ -17,6 +17,9 @@ <?= $this->a(t('Currency rates'), 'currency', 'index') ?> </li> <li> + <?= $this->a(t('Integrations'), 'config', 'integrations') ?> + </li> + <li> <?= $this->a(t('Webhooks'), 'config', 'webhook') ?> </li> <li> diff --git a/app/Template/event/task_assignee_change.php b/app/Template/event/task_assignee_change.php index 6eac412b..22ed936b 100644 --- a/app/Template/event/task_assignee_change.php +++ b/app/Template/event/task_assignee_change.php @@ -1,9 +1,15 @@ <p class="activity-title"> - <?= e('%s changed the assignee of the task %s to %s', - $this->e($author), - $this->a(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), - $this->e($task['assignee_name'] ?: $task['assignee_username']) - ) ?> + <?php $assignee = $task['assignee_name'] ?: $task['assignee_username'] ?> + + <?php if (! empty($assignee)): ?> + <?= e('%s changed the assignee of the task %s to %s', + $this->e($author), + $this->a(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])), + $this->e($assignee) + ) ?> + <?php else: ?> + <?= e('%s remove the assignee of the task %s', $this->e($author), $this->a(t('#%d', $task['id']), 'task', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']))) ?> + <?php endif ?> </p> <p class="activity-description"> <em><?= $this->e($task['title']) ?></em> diff --git a/docs/slack.markdown b/docs/slack.markdown new file mode 100644 index 00000000..7d7777eb --- /dev/null +++ b/docs/slack.markdown @@ -0,0 +1,21 @@ +Slack integration +================= + +Send notifications to a channel +------------------------------- + +Example of notifications: + +![Slack notification](http://kanboard.net/screenshots/documentation/slack-notification.png) + +This feature use the [Incoming webhook](https://api.slack.com/incoming-webhooks) system of Slack. + +### Configure Slack + +![Slack webhook creation](http://kanboard.net/screenshots/documentation/slack-add-incoming-webhook.png) + +1. Click on the Team dropdown and choose **Configure Integrations** +2. On the list of services, scroll-down and choose **DIY Integrations & Customizations > Incoming WebHooks** +3. Copy the webhook url to the Kanboard settings page: **Settings > Integrations > Slack** + + |