summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrederic Guillot <fred@kanboard.net>2015-03-28 21:37:53 -0400
committerFrederic Guillot <fred@kanboard.net>2015-03-28 21:37:53 -0400
commit5536f6c6ce591ba05a169d2e33b6fb240378d8a4 (patch)
treebac0cb1563d8258965384b97be8d425578f71ef6
parentf9891a966fb87d2112f174b7c3a1b3a705b73bdd (diff)
Add Slack integration
-rw-r--r--README.markdown1
-rw-r--r--app/Controller/Config.php17
-rw-r--r--app/Core/HttpClient.php67
-rw-r--r--app/Integration/SlackWebhook.php43
-rw-r--r--app/Locale/da_DK/translations.php4
-rw-r--r--app/Locale/de_DE/translations.php4
-rw-r--r--app/Locale/es_ES/translations.php4
-rw-r--r--app/Locale/fi_FI/translations.php4
-rw-r--r--app/Locale/fr_FR/translations.php4
-rw-r--r--app/Locale/hu_HU/translations.php4
-rw-r--r--app/Locale/it_IT/translations.php4
-rw-r--r--app/Locale/ja_JP/translations.php4
-rw-r--r--app/Locale/nl_NL/translations.php4
-rw-r--r--app/Locale/pl_PL/translations.php4
-rw-r--r--app/Locale/pt_BR/translations.php4
-rw-r--r--app/Locale/ru_RU/translations.php4
-rw-r--r--app/Locale/sr_Latn_RS/translations.php4
-rw-r--r--app/Locale/sv_SE/translations.php4
-rw-r--r--app/Locale/th_TH/translations.php4
-rw-r--r--app/Locale/tr_TR/translations.php4
-rw-r--r--app/Locale/zh_CN/translations.php4
-rw-r--r--app/Model/ProjectActivity.php8
-rw-r--r--app/Model/Webhook.php39
-rw-r--r--app/Schema/Mysql.php9
-rw-r--r--app/Schema/Postgres.php9
-rw-r--r--app/Schema/Sqlite.php9
-rw-r--r--app/ServiceProvider/ClassProvider.php2
-rw-r--r--app/Subscriber/ProjectActivitySubscriber.php9
-rw-r--r--app/Template/config/integrations.php22
-rw-r--r--app/Template/config/sidebar.php3
-rw-r--r--app/Template/event/task_assignee_change.php16
-rw-r--r--docs/slack.markdown21
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').' &gt; '.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('&quot;', '"', $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>&nbsp;<?= 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**
+
+