summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrederic Guillot <fred@kanboard.net>2015-04-18 18:44:45 -0400
committerFrederic Guillot <fred@kanboard.net>2015-04-18 18:44:45 -0400
commit370b5a0fd7c1dba60e3b973506ba087adba42be0 (patch)
tree8da109b4fc90062d6eebb69d4ae2efca4da1bac3
parentf53bb88d10836e5c31efb958683d8bf3829eecbf (diff)
Add Slack and Hipchat integrations for each projects
-rw-r--r--app/Controller/Base.php1
-rw-r--r--app/Controller/Project.php11
-rw-r--r--app/Core/HttpClient.php34
-rw-r--r--app/Integration/Base.php1
-rw-r--r--app/Integration/Hipchat.php90
-rw-r--r--app/Integration/SlackWebhook.php60
-rw-r--r--app/Model/ProjectIntegration.php66
-rw-r--r--app/Schema/Mysql.php20
-rw-r--r--app/Schema/Postgres.php19
-rw-r--r--app/Schema/Sqlite.php19
-rw-r--r--app/ServiceProvider/ClassProvider.php1
-rw-r--r--app/Subscriber/ProjectActivitySubscriber.php28
-rw-r--r--app/Template/project/integrations.php73
-rw-r--r--docs/hipchat.markdown11
-rw-r--r--docs/slack.markdown11
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('&quot;', '"', $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('&quot;', '"', $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>&nbsp;<?= 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"/>&nbsp;<?= 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>&nbsp;<?= 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>&nbsp;<?= 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"/>&nbsp;<?= 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>&nbsp;<?= 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>&nbsp;<?= 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.