diff options
author | Frederic Guillot <fred@kanboard.net> | 2015-04-18 18:44:45 -0400 |
---|---|---|
committer | Frederic Guillot <fred@kanboard.net> | 2015-04-18 18:44:45 -0400 |
commit | 370b5a0fd7c1dba60e3b973506ba087adba42be0 (patch) | |
tree | 8da109b4fc90062d6eebb69d4ae2efca4da1bac3 | |
parent | f53bb88d10836e5c31efb958683d8bf3829eecbf (diff) |
Add Slack and Hipchat integrations for each projects
-rw-r--r-- | app/Controller/Base.php | 1 | ||||
-rw-r--r-- | app/Controller/Project.php | 11 | ||||
-rw-r--r-- | app/Core/HttpClient.php | 34 | ||||
-rw-r--r-- | app/Integration/Base.php | 1 | ||||
-rw-r--r-- | app/Integration/Hipchat.php | 90 | ||||
-rw-r--r-- | app/Integration/SlackWebhook.php | 60 | ||||
-rw-r--r-- | app/Model/ProjectIntegration.php | 66 | ||||
-rw-r--r-- | app/Schema/Mysql.php | 20 | ||||
-rw-r--r-- | app/Schema/Postgres.php | 19 | ||||
-rw-r--r-- | app/Schema/Sqlite.php | 19 | ||||
-rw-r--r-- | app/ServiceProvider/ClassProvider.php | 1 | ||||
-rw-r--r-- | app/Subscriber/ProjectActivitySubscriber.php | 28 | ||||
-rw-r--r-- | app/Template/project/integrations.php | 73 | ||||
-rw-r--r-- | docs/hipchat.markdown | 11 | ||||
-rw-r--r-- | docs/slack.markdown | 11 |
15 files changed, 366 insertions, 79 deletions
diff --git a/app/Controller/Base.php b/app/Controller/Base.php index f4b99a79..869d5d59 100644 --- a/app/Controller/Base.php +++ b/app/Controller/Base.php @@ -43,6 +43,7 @@ use Symfony\Component\EventDispatcher\Event; * @property \Model\ProjectAnalytic $projectAnalytic * @property \Model\ProjectActivity $projectActivity * @property \Model\ProjectDailySummary $projectDailySummary + * @property \Model\ProjectIntegration $projectIntegration * @property \Model\Subtask $subtask * @property \Model\SubtaskForecast $subtaskForecast * @property \Model\Swimlane $swimlane diff --git a/app/Controller/Project.php b/app/Controller/Project.php index 4e01271a..165ed2df 100644 --- a/app/Controller/Project.php +++ b/app/Controller/Project.php @@ -95,10 +95,21 @@ class Project extends Base { $project = $this->getProject(); + if ($this->request->isPost()) { + $params = $this->request->getValues(); + $params += array('hipchat' => 0, 'slack' => 0); + $this->projectIntegration->saveParameters($project['id'], $params); + } + + $values = $this->projectIntegration->getParameters($project['id']); + $values += array('hipchat_api_url' => 'https://api.hipchat.com'); + $this->response->html($this->projectLayout('project/integrations', array( 'project' => $project, 'title' => t('Integrations'), 'webhook_token' => $this->config->get('webhook_token'), + 'values' => $values, + 'errors' => array(), ))); } diff --git a/app/Core/HttpClient.php b/app/Core/HttpClient.php index e1d90858..0803ec7a 100644 --- a/app/Core/HttpClient.php +++ b/app/Core/HttpClient.php @@ -2,6 +2,8 @@ namespace Core; +use Pimple\Container; + /** * HTTP client * @@ -32,15 +34,33 @@ class HttpClient const HTTP_USER_AGENT = 'Kanboard Webhook'; /** + * Container instance + * + * @access protected + * @var \Pimple\Container + */ + protected $container; + + /** + * Constructor + * + * @access public + * @param \Pimple\Container $container + */ + public function __construct(Container $container) + { + $this->container = $container; + } + + /** * Send a POST HTTP request * - * @static * @access public * @param string $url * @param array $data * @return string */ - public static function post($url, array $data) + public function post($url, array $data) { if (empty($url)) { return ''; @@ -63,6 +83,14 @@ class HttpClient ) )); - return @file_get_contents(trim($url), false, $context); + $response = @file_get_contents(trim($url), false, $context); + + if (DEBUG) { + $this->container['logger']->debug($url); + $this->container['logger']->debug(var_export($data, true)); + $this->container['logger']->debug($response); + } + + return $response; } } diff --git a/app/Integration/Base.php b/app/Integration/Base.php index c6387fe2..9daa3eb0 100644 --- a/app/Integration/Base.php +++ b/app/Integration/Base.php @@ -11,6 +11,7 @@ use Pimple\Container; * @author Frederic Guillot * * @property \Model\ProjectActivity $projectActivity + * @property \Model\ProjectIntegration $projectIntegration * @property \Model\Task $task * @property \Model\TaskFinder $taskFinder * @property \Model\User $user diff --git a/app/Integration/Hipchat.php b/app/Integration/Hipchat.php index 1306af6d..d0a48e42 100644 --- a/app/Integration/Hipchat.php +++ b/app/Integration/Hipchat.php @@ -3,7 +3,7 @@ namespace Integration; /** - * Hipchat Webhook + * Hipchat * * @package integration * @author Frederic Guillot @@ -11,7 +11,45 @@ namespace Integration; class Hipchat extends Base { /** - * Send message to the Hipchat room + * Return true if Hipchat is enabled for this project or globally + * + * @access public + * @param integer $project_id + * @return boolean + */ + public function isActivated($project_id) + { + return $this->config->get('integration_hipchat') == 1 || $this->projectIntegration->hasValue($project_id, 'hipchat', 1); + } + + /** + * Get API parameters + * + * @access public + * @param integer $project_id + * @return array + */ + public function getParameters($project_id) + { + if ($this->config->get('integration_hipchat') == 1) { + return array( + 'api_url' => $this->config->get('integration_hipchat_api_url'), + 'room_id' => $this->config->get('integration_hipchat_room_id'), + 'room_token' => $this->config->get('integration_hipchat_room_token'), + ); + } + + $options = $this->projectIntegration->getParameters($project_id); + + return array( + 'api_url' => $options['hipchat_api_url'], + 'room_id' => $options['hipchat_room_id'], + 'room_token' => $options['hipchat_room_token'], + ); + } + + /** + * Send the notification if activated * * @access public * @param integer $project_id Project id @@ -21,33 +59,37 @@ class Hipchat extends Base */ public function notify($project_id, $task_id, $event_name, array $event) { - $project = $this->project->getbyId($project_id); + if ($this->isActivated($project_id)) { - $event['event_name'] = $event_name; - $event['author'] = $this->user->getFullname($this->session['user']); + $params = $this->getParameters($project_id); + $project = $this->project->getbyId($project_id); - $html = '<img src="http://kanboard.net/assets/img/favicon-32x32.png"/>'; - $html .= '<strong>'.$project['name'].'</strong><br/>'; - $html .= $this->projectActivity->getTitle($event); + $event['event_name'] = $event_name; + $event['author'] = $this->user->getFullname($this->session['user']); - if ($this->config->get('application_url')) { - $html .= '<br/><a href="'.$this->config->get('application_url'); - $html .= $this->helper->u('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id)).'">'; - $html .= t('view the task on Kanboard').'</a>'; - } + $html = '<img src="http://kanboard.net/assets/img/favicon-32x32.png"/>'; + $html .= '<strong>'.$project['name'].'</strong>'.(isset($event['task']['title']) ? '<br/>'.$event['task']['title'] : '').'<br/>'; + $html .= $this->projectActivity->getTitle($event); - $payload = array( - 'message' => $html, - 'color' => 'yellow', - ); + if ($this->config->get('application_url')) { + $html .= '<br/><a href="'.$this->config->get('application_url'); + $html .= $this->helper->u('task', 'show', array('task_id' => $task_id, 'project_id' => $project_id)).'">'; + $html .= t('view the task on Kanboard').'</a>'; + } - $url = sprintf( - '%s/v2/room/%s/notification?auth_token=%s', - $this->config->get('integration_hipchat_api_url'), - $this->config->get('integration_hipchat_room_id'), - $this->config->get('integration_hipchat_room_token') - ); + $payload = array( + 'message' => $html, + 'color' => 'yellow', + ); - $this->httpClient->post($url, $payload); + $url = sprintf( + '%s/v2/room/%s/notification?auth_token=%s', + $params['api_url'], + $params['room_id'], + $params['room_token'] + ); + + $this->httpClient->post($url, $payload); + } } } diff --git a/app/Integration/SlackWebhook.php b/app/Integration/SlackWebhook.php index 1c2ea781..b64096fb 100644 --- a/app/Integration/SlackWebhook.php +++ b/app/Integration/SlackWebhook.php @@ -11,6 +11,35 @@ namespace Integration; class SlackWebhook extends Base { /** + * Return true if Slack is enabled for this project or globally + * + * @access public + * @param integer $project_id + * @return boolean + */ + public function isActivated($project_id) + { + return $this->config->get('integration_slack_webhook') == 1 || $this->projectIntegration->hasValue($project_id, 'slack', 1); + } + + /** + * Get wehbook url + * + * @access public + * @param integer $project_id + * @return string + */ + public function getWebhookUrl($project_id) + { + if ($this->config->get('integration_slack_webhook') == 1) { + return $this->config->get('integration_slack_webhook_url'); + } + + $options = $this->projectIntegration->getParameters($project_id); + return $options['slack_webhook_url']; + } + + /** * Send message to the incoming Slack webhook * * @access public @@ -21,23 +50,26 @@ class SlackWebhook extends Base */ public function notify($project_id, $task_id, $event_name, array $event) { - $project = $this->project->getbyId($project_id); + if ($this->isActivated($project_id)) { - $event['event_name'] = $event_name; - $event['author'] = $this->user->getFullname($this->session['user']); + $project = $this->project->getbyId($project_id); - $payload = array( - 'text' => '*['.$project['name'].']* '.str_replace('"', '"', $this->projectActivity->getTitle($event)), - 'username' => 'Kanboard', - 'icon_url' => 'http://kanboard.net/assets/img/favicon.png', - ); + $event['event_name'] = $event_name; + $event['author'] = $this->user->getFullname($this->session['user']); - 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').'>'; - } + $payload = array( + 'text' => '*['.$project['name'].']* '.str_replace('"', '"', $this->projectActivity->getTitle($event)).(isset($event['task']['title']) ? ' ('.$event['task']['title'].')' : ''), + 'username' => 'Kanboard', + 'icon_url' => 'http://kanboard.net/assets/img/favicon.png', + ); - $this->httpClient->post($this->config->get('integration_slack_webhook_url'), $payload); + 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->getWebhookUrl($project_id), $payload); + } } } diff --git a/app/Model/ProjectIntegration.php b/app/Model/ProjectIntegration.php new file mode 100644 index 00000000..98ff8d4c --- /dev/null +++ b/app/Model/ProjectIntegration.php @@ -0,0 +1,66 @@ +<?php + +namespace Model; + +/** + * Project integration + * + * @package model + * @author Frederic Guillot + */ +class ProjectIntegration extends Base +{ + /** + * SQL table name + * + * @var string + */ + const TABLE = 'project_integrations'; + + /** + * Get all parameters for a project + * + * @access public + * @param integer $project_id + * @return array + */ + public function getParameters($project_id) + { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->findOne() ?: array(); + } + + /** + * Save parameters for a project + * + * @access public + * @param integer $project_id + * @param array $values + * @return boolean + */ + public function saveParameters($project_id, array $values) + { + if ($this->db->table(self::TABLE)->eq('project_id', $project_id)->count() === 1) { + return $this->db->table(self::TABLE)->eq('project_id', $project_id)->update($values); + } + + return $this->db->table(self::TABLE)->insert($values + array('project_id' => $project_id)); + } + + /** + * Check if a project has the given parameter/value + * + * @access public + * @param integer $project_id + * @param string $option + * @param string $value + * @return boolean + */ + public function hasValue($project_id, $option, $value) + { + return $this->db + ->table(self::TABLE) + ->eq('project_id', $project_id) + ->eq($option, $value) + ->count() === 1; + } +} diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index 6ad6dc51..3e67387d 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,25 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 64; +const VERSION = 65; + +function version_65($pdo) +{ + $pdo->exec(" + CREATE TABLE project_integrations ( + `id` INT NOT NULL AUTO_INCREMENT, + `project_id` INT NOT NULL UNIQUE, + `hipchat` TINYINT(1) DEFAULT 0, + `hipchat_api_url` VARCHAR(255) DEFAULT 'https://api.hipchat.com', + `hipchat_room_id` VARCHAR(255), + `hipchat_room_token` VARCHAR(255), + `slack` TINYINT(1) DEFAULT 0, + `slack_webhook_url` VARCHAR(255), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE, + PRIMARY KEY(id) + ) ENGINE=InnoDB CHARSET=utf8 + "); +} function version_64($pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index b5cf72a6..6973b266 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,24 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 45; +const VERSION = 46; + +function version_46($pdo) +{ + $pdo->exec(" + CREATE TABLE project_integrations ( + id SERIAL PRIMARY KEY, + project_id INTEGER NOT NULL UNIQUE, + hipchat BOOLEAN DEFAULT '0', + hipchat_api_url VARCHAR(255) DEFAULT 'https://api.hipchat.com', + hipchat_room_id VARCHAR(255), + hipchat_room_token VARCHAR(255), + slack BOOLEAN DEFAULT '0', + slack_webhook_url VARCHAR(255), + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) + "); +} function version_45($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index fb1d7d29..c4ebd98a 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,24 @@ use Core\Security; use PDO; use Model\Link; -const VERSION = 63; +const VERSION = 64; + +function version_64($pdo) +{ + $pdo->exec(" + CREATE TABLE project_integrations ( + id INTEGER PRIMARY KEY, + project_id INTEGER NOT NULL UNIQUE, + hipchat INTEGER DEFAULT 0, + hipchat_api_url TEXT DEFAULT 'https://api.hipchat.com', + hipchat_room_id TEXT, + hipchat_room_token TEXT, + slack INTEGER DEFAULT 0, + slack_webhook_url TEXT, + FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE + ) + "); +} function version_63($pdo) { diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index 6a12ea5a..b08d506c 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -34,6 +34,7 @@ class ClassProvider implements ServiceProviderInterface 'ProjectAnalytic', 'ProjectDuplication', 'ProjectDailySummary', + 'ProjectIntegration', 'ProjectPermission', 'Subtask', 'SubtaskExport', diff --git a/app/Subscriber/ProjectActivitySubscriber.php b/app/Subscriber/ProjectActivitySubscriber.php index 42314637..696b958b 100644 --- a/app/Subscriber/ProjectActivitySubscriber.php +++ b/app/Subscriber/ProjectActivitySubscriber.php @@ -49,26 +49,22 @@ class ProjectActivitySubscriber extends Base implements EventSubscriberInterface private function sendSlackNotification($event_name, array $values) { - if ($this->config->get('integration_slack_webhook') == 1) { - $this->slackWebhook->notify( - $values['task']['project_id'], - $values['task']['id'], - $event_name, - $values - ); - } + $this->slackWebhook->notify( + $values['task']['project_id'], + $values['task']['id'], + $event_name, + $values + ); } private function sendHipchatNotification($event_name, array $values) { - if ($this->config->get('integration_hipchat') == 1) { - $this->hipchat->notify( - $values['task']['project_id'], - $values['task']['id'], - $event_name, - $values - ); - } + $this->hipchat->notify( + $values['task']['project_id'], + $values['task']['id'], + $event_name, + $values + ); } private function getValues(GenericEvent $event) diff --git a/app/Template/project/integrations.php b/app/Template/project/integrations.php index 4f6553ad..da27e430 100644 --- a/app/Template/project/integrations.php +++ b/app/Template/project/integrations.php @@ -2,20 +2,63 @@ <h2><?= t('Integration with third-party services') ?></h2> </div> -<h3><i class="fa fa-github fa-fw"></i> <?= t('Github webhooks') ?></h3> -<div class="listing"> -<input type="text" class="auto-select" readonly="readonly" value="<?= $this->getCurrentBaseUrl().$this->u('webhook', 'github', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/> -<p class="form-help"><a href="http://kanboard.net/documentation/github-webhooks" target="_blank"><?= t('Help on Github webhooks') ?></a></p> -</div> +<form method="post" action="<?= $this->u('project', 'integration', array('project_id' => $project['id'])) ?>" autocomplete="off"> + <?= $this->formCsrf() ?> -<h3><img src="assets/img/gitlab-icon.png"/> <?= t('Gitlab webhooks') ?></h3> -<div class="listing"> -<input type="text" class="auto-select" readonly="readonly" value="<?= $this->getCurrentBaseUrl().$this->u('webhook', 'gitlab', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/> -<p class="form-help"><a href="http://kanboard.net/documentation/gitlab-webhooks" target="_blank"><?= t('Help on Gitlab webhooks') ?></a></p> -</div> -<h3><i class="fa fa-bitbucket fa-fw"></i> <?= t('Bitbucket webhooks') ?></h3> -<div class="listing"> -<input type="text" class="auto-select" readonly="readonly" value="<?= $this->getCurrentBaseUrl().$this->u('webhook', 'bitbucket', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/> -<p class="form-help"><a href="http://kanboard.net/documentation/bitbucket-webhooks" target="_blank"><?= t('Help on Bitbucket webhooks') ?></a></p> -</div>
\ No newline at end of file + <h3><i class="fa fa-github fa-fw"></i> <?= t('Github webhooks') ?></h3> + <div class="listing"> + <input type="text" class="auto-select" readonly="readonly" value="<?= $this->getCurrentBaseUrl().$this->u('webhook', 'github', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/> + <p class="form-help"><a href="http://kanboard.net/documentation/github-webhooks" target="_blank"><?= t('Help on Github webhooks') ?></a></p> + </div> + + + <h3><img src="assets/img/gitlab-icon.png"/> <?= t('Gitlab webhooks') ?></h3> + <div class="listing"> + <input type="text" class="auto-select" readonly="readonly" value="<?= $this->getCurrentBaseUrl().$this->u('webhook', 'gitlab', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/> + <p class="form-help"><a href="http://kanboard.net/documentation/gitlab-webhooks" target="_blank"><?= t('Help on Gitlab webhooks') ?></a></p> + </div> + + + <h3><i class="fa fa-bitbucket fa-fw"></i> <?= t('Bitbucket webhooks') ?></h3> + <div class="listing"> + <input type="text" class="auto-select" readonly="readonly" value="<?= $this->getCurrentBaseUrl().$this->u('webhook', 'bitbucket', array('token' => $webhook_token, 'project_id' => $project['id'])) ?>"/><br/> + <p class="form-help"><a href="http://kanboard.net/documentation/bitbucket-webhooks" target="_blank"><?= t('Help on Bitbucket webhooks') ?></a></p> + </div> + + + <h3><img src="assets/img/hipchat-icon.png"/> <?= t('Hipchat') ?></h3> + <div class="listing"> + <?= $this->formCheckbox('hipchat', t('Send notifications to Hipchat'), 1, isset($values['hipchat']) && $values['hipchat'] == 1) ?> + + <?= $this->formLabel(t('API URL'), 'hipchat_api_url') ?> + <?= $this->formText('hipchat_api_url', $values, $errors) ?> + + <?= $this->formLabel(t('Room API ID or name'), 'hipchat_room_id') ?> + <?= $this->formText('hipchat_room_id', $values, $errors) ?> + + <?= $this->formLabel(t('Room notification token'), 'hipchat_room_token') ?> + <?= $this->formText('hipchat_room_token', $values, $errors) ?> + + <p class="form-help"><a href="http://kanboard.net/documentation/hipchat" target="_blank"><?= t('Help on Hipchat integration') ?></a></p> + + <div class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> + </div> + + + <h3><i class="fa fa-slack fa-fw"></i> <?= t('Slack') ?></h3> + <div class="listing"> + <?= $this->formCheckbox('slack', t('Send notifications to a Slack channel'), 1, isset($values['slack']) && $values['slack'] == 1) ?> + + <?= $this->formLabel(t('Webhook URL'), 'slack_webhook_url') ?> + <?= $this->formText('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 class="form-actions"> + <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> + </div> + </div> +</form>
\ No newline at end of file diff --git a/docs/hipchat.markdown b/docs/hipchat.markdown index 45d93eb2..66cf9fdc 100644 --- a/docs/hipchat.markdown +++ b/docs/hipchat.markdown @@ -1,6 +1,13 @@ Hipchat integration =================== +You can send notifications to Hipchat for all projects or only for specific projects. + +- To send notifications for all projects, go to **Settings > Integrations > Hipchat** +- To send notifications for only some projects, go to **Project settings > Integrations > Hipchat** + +Each project can send notifications to a separate room. + Send notifications to a room ----------------------------- @@ -23,9 +30,9 @@ This feature use the room notification token system of Hipchat. ![Hipchat settings](http://kanboard.net/screenshots/documentation/hipchat-settings.png) -1. Go to **Settings > Integrations > Hipchat** +1. Go to **Settings > Integrations > Hipchat** or **Project settings > Integrations > Hipchat** 2. Replace the API url if you use the self-hosted version of Hipchat 3. Set the room name or the room API ID 4. Copy and paste the token generated previously -Now, all Kanboard events will be sent to the Hipchat room. +Now, Kanboard events will be sent to the Hipchat room. diff --git a/docs/slack.markdown b/docs/slack.markdown index 89e3006b..af60d38e 100644 --- a/docs/slack.markdown +++ b/docs/slack.markdown @@ -1,6 +1,13 @@ Slack integration ================= +You can send notifications to Slack for all projects or only for specific projects. + +- To send notifications for all projects, go to **Settings > Integrations > Slack** +- To send notifications for only some projects, go to **Project settings > Integrations > Slack** + +Each project can send notifications to a separate channel. + Send notifications to a channel ------------------------------- @@ -16,6 +23,6 @@ This feature use the [Incoming webhook](https://api.slack.com/incoming-webhooks) 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** +3. Copy the webhook url to the Kanboard settings page: **Settings > Integrations > Slack** or **Project settings > Integrations > Slack** -Now, all Kanboard events will be sent to the Slack channel. +Now, Kanboard events will be sent to the Slack channel. |