Improve webhooks to call external url on task creation/modification

This commit is contained in:
Frédéric Guillot 2014-07-21 20:32:12 -02:30
parent 4ae655ced3
commit 9e1dcf21dc
18 changed files with 320 additions and 14 deletions

View File

@ -151,6 +151,7 @@ abstract class Base
// Attach events
$this->action->attachEvents();
$this->project->attachEvents();
$this->webhook->attachEvents();
}
/**

View File

@ -6,12 +6,14 @@ use Core\Listener;
use Model\Project;
/**
* Task modification listener
* Project modification date listener
*
* @package events
* Update the last modified field for a project
*
* @package event
* @author Frederic Guillot
*/
class TaskModification implements Listener
class ProjectModificationDate implements Listener
{
/**
* Project model

View File

@ -0,0 +1,49 @@
<?php
namespace Event;
use Core\Listener;
use Model\Webhook;
/**
* Webhook task events
*
* @package event
* @author Frederic Guillot
*/
class WebhookListener implements Listener
{
/**
* Webhook model
*
* @accesss private
* @var \Model\Webhook
*/
private $webhook;
/**
* Constructor
*
* @access public
* @param string $url URL to call
* @param \Model\Webhook $webhook Webhook model instance
*/
public function __construct($url, Webhook $webhook)
{
$this->url = $url;
$this->webhook = $webhook;
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function execute(array $data)
{
$this->webhook->notify($this->url, $data);
return true;
}
}

View File

@ -396,4 +396,6 @@ return array(
// 'Creator' => '',
// 'Modification date' => '',
// 'Completion date' => '',
// 'Webhook URL for task creation' => '',
// 'Webhook URL for task modification' => '',
);

View File

@ -395,4 +395,6 @@ return array(
// 'Creator' => '',
// 'Modification date' => '',
// 'Completion date' => '',
// 'Webhook URL for task creation' => '',
// 'Webhook URL for task modification' => '',
);

View File

@ -393,4 +393,6 @@ return array(
'Creator' => 'Créateur',
'Modification date' => 'Date de modification',
'Completion date' => 'Date de complétion',
'Webhook URL for task creation' => 'URL du webhook pour la création de tâche',
'Webhook URL for task modification' => 'URL du webhook pour la modification de tâche',
);

View File

@ -396,4 +396,6 @@ return array(
// 'Creator' => '',
// 'Modification date' => '',
// 'Completion date' => '',
// 'Webhook URL for task creation' => '',
// 'Webhook URL for task modification' => '',
);

View File

@ -393,4 +393,6 @@ return array(
// 'Creator' => '',
// 'Modification date' => '',
// 'Completion date' => '',
// 'Webhook URL for task creation' => '',
// 'Webhook URL for task modification' => '',
);

View File

@ -395,4 +395,6 @@ return array(
// 'Creator' => '',
// 'Modification date' => '',
// 'Completion date' => '',
// 'Webhook URL for task creation' => '',
// 'Webhook URL for task modification' => '',
);

View File

@ -401,4 +401,6 @@ return array(
// 'Creator' => '',
// 'Modification date' => '',
// 'Completion date' => '',
// 'Webhook URL for task creation' => '',
// 'Webhook URL for task modification' => '',
);

View File

@ -4,7 +4,7 @@ namespace Model;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use Event\TaskModification;
use Event\ProjectModificationDate;
use Core\Security;
/**
@ -575,7 +575,7 @@ class Project extends Base
Task::EVENT_OPEN,
);
$listener = new TaskModification($this);
$listener = new ProjectModificationDate($this);
foreach ($events as $event_name) {
$this->event->attach($event_name, $listener);

154
app/Model/Webhook.php Normal file
View File

@ -0,0 +1,154 @@
<?php
namespace Model;
use Event\WebhookListener;
/**
* Webhook model
*
* @package model
* @author Frederic Guillot
*/
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';
/**
* URL to call for task creation
*
* @access private
* @var string
*/
private $url_task_creation = '';
/**
* URL to call for task modification
*
* @access private
* @var string
*/
private $url_task_modification = '';
/**
* Webook token
*
* @access private
* @var string
*/
private $token = '';
/**
* Attach events
*
* @access public
*/
public function attachEvents()
{
$config = new Config($this->db, $this->event);
$this->url_task_creation = $config->get('webhooks_url_task_creation');
$this->url_task_modification = $config->get('webhooks_url_task_modification');
$this->token = $config->get('webhooks_token');
if ($this->url_task_creation) {
$this->attachCreateEvents();
}
if ($this->url_task_modification) {
$this->attachUpdateEvents();
}
}
/**
* Attach events for task modification
*
* @access public
*/
public function attachUpdateEvents()
{
$events = array(
Task::EVENT_UPDATE,
Task::EVENT_CLOSE,
Task::EVENT_OPEN,
);
$listener = new WebhookListener($this->url_task_modification, $this);
foreach ($events as $event_name) {
$this->event->attach($event_name, $listener);
}
}
/**
* Attach events for task creation
*
* @access public
*/
public function attachCreateEvents()
{
$events = array(
Task::EVENT_CREATE,
);
$listener = new WebhookListener($this->url_task_creation, $this);
foreach ($events as $event_name) {
$this->event->attach($event_name, $listener);
}
}
/**
* Call the external URL
*
* @access public
* @param string $url URL to call
* @param array $task Task data
*/
public function notify($url, array $task)
{
$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='.$this->token;
}
else {
$url .= '?token='.$this->token;
}
@file_get_contents($url, false, $context);
}
}

View File

@ -4,7 +4,13 @@ namespace Schema;
use Core\Security;
const VERSION = 21;
const VERSION = 22;
function version_22($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_modification VARCHAR(255)");
$pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_creation VARCHAR(255)");
}
function version_21($pdo)
{

View File

@ -4,7 +4,13 @@ namespace Schema;
use Core\Security;
const VERSION = 2;
const VERSION = 3;
function version_3($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_modification VARCHAR(255)");
$pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_creation VARCHAR(255)");
}
function version_2($pdo)
{

View File

@ -4,7 +4,13 @@ namespace Schema;
use Core\Security;
const VERSION = 21;
const VERSION = 22;
function version_22($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_modification TEXT");
$pdo->exec("ALTER TABLE config ADD COLUMN webhooks_url_task_creation TEXT");
}
function version_21($pdo)
{

View File

@ -15,6 +15,12 @@
<?= Helper\form_label(t('Timezone'), 'timezone') ?>
<?= Helper\form_select('timezone', $timezones, $values, $errors) ?><br/>
<?= Helper\form_label(t('Webhook URL for task creation'), 'webhooks_url_task_creation') ?>
<?= Helper\form_text('webhooks_url_task_creation', $values, $errors) ?><br/>
<?= Helper\form_label(t('Webhook URL for task modification'), 'webhooks_url_task_modification') ?>
<?= Helper\form_text('webhooks_url_task_modification', $values, $errors) ?><br/>
<div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div>

View File

@ -1,7 +1,10 @@
Webhooks
========
Webhooks are useful to perform actions from external applications (shell-scripts, git hooks...).
Webhooks are useful to perform actions with external applications.
- Webhooks can be used to create a task by calling a simple URL (You can also do that by using the API)
- An external URL can be called automatically when a task is created or modified
How to create a task with a webhook?
------------------------------------
@ -16,17 +19,15 @@ curl "http://myserver/?controller=task&action=add&token=superSecretToken&title=m
curl "http://myserver/?controller=task&action=add&token=superSecretToken&title=task123&project_id=3&column_id=7&color_id=red"
```
Available responses
-------------------
### Available responses
- When a task is created successfully, Kanboard return the message "OK" in plain text.
- However if the task creation fail, you will got a "FAILED" message.
- If the token is wrong, you got a "Not Authorized" message and a HTTP status code 401.
Available parameters
--------------------
### Available parameters
Base url: `http://YOUR_SERVER_HOSTNAME/?controller=task&action=add`
Base URL: `http://YOUR_SERVER_HOSTNAME/?controller=task&action=add`
- `token`: Token displayed on the settings page (required)
- `title`: Task title (required)
@ -37,3 +38,61 @@ Base url: `http://YOUR_SERVER_HOSTNAME/?controller=task&action=add`
- `column_id`: Column on the board (Get the column id from the projects page, mouse over on the column name)
Only the token and the title parameters are mandatory. The different id can also be found in the database.
How to call an external URL when a task is created or updated?
--------------------------------------------------------------
- There is two events available: **task creation** and **task modification**
- External URLs can be defined on the settings page
- When an event is triggered Kanboard call automatically the predefined URL
- The task data encoded in JSON is sent with a POST HTTP request
- The webhook token is also sent as a query string parameter, so you can check if the request is not usurped, it's also better if you use HTTPS.
- **Your custom URL must answer in less than 1 second**, those requests are synchronous (PHP limitation) and that can slow down the application if your script is too slow!
### Quick example with PHP
Start by creating a basic PHP script `index.php`:
```php
<?php
$body = file_get_contents('php://input');
file_put_contents('/tmp/webhook', $body);
```
This script dump the task data to the file `/tmp/webhook`.
Now run a webserver from the command line:
```bash
php -S 127.0.0.1:8081
```
After that, go the settings page of Kanboard, enter the right URL here `http://127.0.0.1:8081/`.
And finally, create a task and you should see the JSON payload in the file.
```javascript
{
"task_id":"2",
"title":"boo",
"description":"",
"project_id":"1",
"owner_id":"0",
"category_id":"0",
"column_id":"2",
"color_id":"yellow",
"score":0,
"date_due":0,
"creator_id":1,
"date_creation":1405981280,
"position":0
}
```
For our example, Kanboard use this request to call your program:
```
POST http:://127.0.0.1:8081/?token=RANDOM_TOKEN_HERE
{... JSON payload ...}
```

View File

@ -13,6 +13,7 @@ use Model\Comment;
use Model\SubTask;
use Model\Board;
use Model\Action;
use Model\Webhook;
$config = new Config($registry->shared('db'), $registry->shared('event'));
$project = new Project($registry->shared('db'), $registry->shared('event'));
@ -23,9 +24,11 @@ $comment = new Comment($registry->shared('db'), $registry->shared('event'));
$subtask = new SubTask($registry->shared('db'), $registry->shared('event'));
$board = new Board($registry->shared('db'), $registry->shared('event'));
$action = new Action($registry->shared('db'), $registry->shared('event'));
$webhook = new Webhook($registry->shared('db'), $registry->shared('event'));
$action->attachEvents();
$project->attachEvents();
$webhook->attachEvents();
$server = new Server;
$server->authentication(array('jsonrpc' => $config->get('api_token')));