Send all Kanboard events to the webhook (breaking change)

This commit is contained in:
Frederic Guillot
2015-05-18 21:04:06 -04:00
parent db95e96f92
commit 6d5ffaa848
30 changed files with 501 additions and 154 deletions

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Skaber', 'Creator' => 'Skaber',
'Modification date' => 'Ændringsdato', 'Modification date' => 'Ændringsdato',
'Completion date' => 'Afslutningsdato', 'Completion date' => 'Afslutningsdato',
'Webhook URL for task creation' => 'Webhook URL for opgave oprettelse',
'Webhook URL for task modification' => 'Webhook URL opgave redigering',
'Clone' => 'Kopier', 'Clone' => 'Kopier',
'Clone Project' => 'Kopier projekt', 'Clone Project' => 'Kopier projekt',
'Project cloned successfully.' => 'Projektet er kopieret.', 'Project cloned successfully.' => 'Projektet er kopieret.',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Erstellt von', 'Creator' => 'Erstellt von',
'Modification date' => 'Änderungsdatum', 'Modification date' => 'Änderungsdatum',
'Completion date' => 'Abschlussdatum', 'Completion date' => 'Abschlussdatum',
'Webhook URL for task creation' => 'Webhook URL zur Aufgabenerstellung',
'Webhook URL for task modification' => 'Webhook URL zur Aufgabenbearbeitung',
'Clone' => 'duplizieren', 'Clone' => 'duplizieren',
'Clone Project' => 'Projekt duplizieren', 'Clone Project' => 'Projekt duplizieren',
'Project cloned successfully.' => 'Projekt wurde dupliziert.', 'Project cloned successfully.' => 'Projekt wurde dupliziert.',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Creador', 'Creator' => 'Creador',
'Modification date' => 'Fecha de modificación', 'Modification date' => 'Fecha de modificación',
'Completion date' => 'Fecha de terminación', 'Completion date' => 'Fecha de terminación',
'Webhook URL for task creation' => 'Disparador Web (Webhook) para la creación de tareas',
'Webhook URL for task modification' => 'Disparador Web (Webhook) para la modificación de tareas',
'Clone' => 'Clonar', 'Clone' => 'Clonar',
'Clone Project' => 'Clonar proyecto', 'Clone Project' => 'Clonar proyecto',
'Project cloned successfully.' => 'Proyecto clonado correctamente', 'Project cloned successfully.' => 'Proyecto clonado correctamente',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Luonut', 'Creator' => 'Luonut',
'Modification date' => 'Muokkauspäivä', 'Modification date' => 'Muokkauspäivä',
'Completion date' => 'Valmistumispäivä', 'Completion date' => 'Valmistumispäivä',
'Webhook URL for task creation' => 'Webhook URL tehtävän luomiselle',
'Webhook URL for task modification' => 'Webhook URL tehtävän muokkaamiselle',
'Clone' => 'Kahdenna', 'Clone' => 'Kahdenna',
'Clone Project' => 'Kahdenna projekti', 'Clone Project' => 'Kahdenna projekti',
'Project cloned successfully.' => 'Projekti kahdennettu onnistuneesti', 'Project cloned successfully.' => 'Projekti kahdennettu onnistuneesti',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Créateur', 'Creator' => 'Créateur',
'Modification date' => 'Date de modification', 'Modification date' => 'Date de modification',
'Completion date' => 'Date de complétion', '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',
'Clone' => 'Clone', 'Clone' => 'Clone',
'Clone Project' => 'Cloner le projet', 'Clone Project' => 'Cloner le projet',
'Project cloned successfully.' => 'Projet cloné avec succès.', 'Project cloned successfully.' => 'Projet cloné avec succès.',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Készítette', 'Creator' => 'Készítette',
'Modification date' => 'Módosítás dátuma', 'Modification date' => 'Módosítás dátuma',
'Completion date' => 'Befejezés határideje', 'Completion date' => 'Befejezés határideje',
'Webhook URL for task creation' => 'Webhook URL a feladat létrehozásakor',
'Webhook URL for task modification' => 'Webhook URL a feladatot módosításakor',
'Clone' => 'Másolat', 'Clone' => 'Másolat',
'Clone Project' => 'Projekt másolása', 'Clone Project' => 'Projekt másolása',
'Project cloned successfully.' => 'A projekt sikeresen másolva.', 'Project cloned successfully.' => 'A projekt sikeresen másolva.',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Creatore', 'Creator' => 'Creatore',
'Modification date' => 'Data di modifica', 'Modification date' => 'Data di modifica',
'Completion date' => 'Data di termine', 'Completion date' => 'Data di termine',
'Webhook URL for task creation' => 'URL del Webhook per la creazione di compiti',
'Webhook URL for task modification' => 'URL del Webhook per la modifica di compiti',
'Clone' => 'Clona', 'Clone' => 'Clona',
'Clone Project' => 'Clona il progetto', 'Clone Project' => 'Clona il progetto',
'Project cloned successfully.' => 'Progetto clonato con successo.', 'Project cloned successfully.' => 'Progetto clonato con successo.',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => '作成者', 'Creator' => '作成者',
'Modification date' => '変更日', 'Modification date' => '変更日',
'Completion date' => '完了日', 'Completion date' => '完了日',
'Webhook URL for task creation' => 'タスク作成の Webhook URL',
'Webhook URL for task modification' => 'タスク変更の Webhook URL',
'Clone' => '複製', 'Clone' => '複製',
'Clone Project' => 'プロジェクトの複製', 'Clone Project' => 'プロジェクトの複製',
'Project cloned successfully.' => 'プロジェクトを複製しました。', 'Project cloned successfully.' => 'プロジェクトを複製しました。',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Aangemaakt door', 'Creator' => 'Aangemaakt door',
'Modification date' => 'Wijzigingsdatum', 'Modification date' => 'Wijzigingsdatum',
'Completion date' => 'Afgerond op', 'Completion date' => 'Afgerond op',
'Webhook URL for task creation' => 'Webhook URL voor aanmaken taak',
'Webhook URL for task modification' => 'Webhook URL voor wijzigen taak',
'Clone' => 'Kloon', 'Clone' => 'Kloon',
'Clone Project' => 'Project klonen', 'Clone Project' => 'Project klonen',
'Project cloned successfully.' => 'Project succesvol gekloond.', 'Project cloned successfully.' => 'Project succesvol gekloond.',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Autor', 'Creator' => 'Autor',
'Modification date' => 'Data modyfyfikacji', 'Modification date' => 'Data modyfyfikacji',
'Completion date' => 'Data ukończenia', 'Completion date' => 'Data ukończenia',
'Webhook URL for task creation' => 'Webhook URL do tworzenia zadań',
'Webhook URL for task modification' => 'Webhook URL do modyfikacji zadań',
'Clone' => 'Sklonuj', 'Clone' => 'Sklonuj',
'Clone Project' => 'Sklonuj projekt', 'Clone Project' => 'Sklonuj projekt',
'Project cloned successfully.' => 'Projekt sklonowany pomyślnie.', 'Project cloned successfully.' => 'Projekt sklonowany pomyślnie.',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Criado por', 'Creator' => 'Criado por',
'Modification date' => 'Data da modificação', 'Modification date' => 'Data da modificação',
'Completion date' => 'Data da finalização', 'Completion date' => 'Data da finalização',
'Webhook URL for task creation' => 'Webhook URL para criação de tarefas',
'Webhook URL for task modification' => 'Webhook URL para modificação de tarefa',
'Clone' => 'Clonar', 'Clone' => 'Clonar',
'Clone Project' => 'Clonar Projeto', 'Clone Project' => 'Clonar Projeto',
'Project cloned successfully.' => 'Projeto clonado com sucesso.', 'Project cloned successfully.' => 'Projeto clonado com sucesso.',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Автор', 'Creator' => 'Автор',
'Modification date' => 'Дата изменения', 'Modification date' => 'Дата изменения',
'Completion date' => 'Дата завершения', 'Completion date' => 'Дата завершения',
'Webhook URL for task creation' => 'Webhook URL для создания задачи',
'Webhook URL for task modification' => 'Webhook URL для изменения задачи',
'Clone' => 'Клонировать', 'Clone' => 'Клонировать',
'Clone Project' => 'Клонировать проект', 'Clone Project' => 'Клонировать проект',
'Project cloned successfully.' => 'Проект клонирован.', 'Project cloned successfully.' => 'Проект клонирован.',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Autor', 'Creator' => 'Autor',
'Modification date' => 'Datum izmene', 'Modification date' => 'Datum izmene',
'Completion date' => 'Datum kompletiranja', 'Completion date' => 'Datum kompletiranja',
'Webhook URL for task creation' => 'Webhook URL zadatka za kreiranje',
'Webhook URL for task modification' => 'Webhook URL zadatka za izmenu',
'Clone' => 'Iskopiraj', 'Clone' => 'Iskopiraj',
'Clone Project' => 'Iskopiraj projekat', 'Clone Project' => 'Iskopiraj projekat',
'Project cloned successfully.' => 'Projekat uspešno iskopiran.', 'Project cloned successfully.' => 'Projekat uspešno iskopiran.',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Skapare', 'Creator' => 'Skapare',
'Modification date' => 'Ändringsdatum', 'Modification date' => 'Ändringsdatum',
'Completion date' => 'Slutfört datum', 'Completion date' => 'Slutfört datum',
'Webhook URL for task creation' => 'Webhook URL för att skapa uppgift',
'Webhook URL for task modification' => 'Webhook URL för att ändra uppgift',
'Clone' => 'Klona', 'Clone' => 'Klona',
'Clone Project' => 'Klona projekt', 'Clone Project' => 'Klona projekt',
'Project cloned successfully.' => 'Projektet har klonats.', 'Project cloned successfully.' => 'Projektet har klonats.',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'ผู้สร้าง', 'Creator' => 'ผู้สร้าง',
'Modification date' => 'วันที่แก้ไข', 'Modification date' => 'วันที่แก้ไข',
'Completion date' => 'วันที่เสร็จสิ้น', 'Completion date' => 'วันที่เสร็จสิ้น',
'Webhook URL for task creation' => 'Webhook URL for task creation',
'Webhook URL for task modification' => 'Webhook URL for task modification',
'Clone' => 'เลียนแบบ', 'Clone' => 'เลียนแบบ',
// 'Clone Project' => '', // 'Clone Project' => '',
'Project cloned successfully.' => 'เลียนแบบโปรเจคเรียบร้อยแล้ว', 'Project cloned successfully.' => 'เลียนแบบโปรเจคเรียบร้อยแล้ว',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => 'Oluşturan', 'Creator' => 'Oluşturan',
'Modification date' => 'Değişiklik tarihi', 'Modification date' => 'Değişiklik tarihi',
'Completion date' => 'Tamamlanma tarihi', 'Completion date' => 'Tamamlanma tarihi',
// 'Webhook URL for task creation' => '',
// 'Webhook URL for task modification' => '',
'Clone' => 'Kopya oluştur', 'Clone' => 'Kopya oluştur',
'Clone Project' => 'Projenin kopyasını oluştur', 'Clone Project' => 'Projenin kopyasını oluştur',
'Project cloned successfully.' => 'Proje kopyası başarıyla oluşturuldu.', 'Project cloned successfully.' => 'Proje kopyası başarıyla oluşturuldu.',

View File

@@ -389,8 +389,6 @@ return array(
'Creator' => '创建者', 'Creator' => '创建者',
'Modification date' => '修改日期', 'Modification date' => '修改日期',
'Completion date' => '完成日期', 'Completion date' => '完成日期',
'Webhook URL for task creation' => '创建任务的Webhook URL',
'Webhook URL for task modification' => '修改任务的Webhook URL',
'Clone' => '克隆', 'Clone' => '克隆',
'Clone Project' => '复制项目', 'Clone Project' => '复制项目',
'Project cloned successfully.' => '成功复制项目。', 'Project cloned successfully.' => '成功复制项目。',

View File

@@ -129,7 +129,9 @@ class Comment extends Base
->eq('id', $values['id']) ->eq('id', $values['id'])
->update(array('comment' => $values['comment'])); ->update(array('comment' => $values['comment']));
$this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new CommentEvent($values)); if ($result) {
$this->container['dispatcher']->dispatch(self::EVENT_UPDATE, new CommentEvent($values));
}
return $result; return $result;
} }

View File

@@ -14,20 +14,23 @@ class Webhook extends Base
* Call the external URL * Call the external URL
* *
* @access public * @access public
* @param string $url URL to call * @param array $values Event payload
* @param array $task Task data
*/ */
public function notify($url, array $task) public function notify(array $values)
{ {
$url = $this->config->get('webhook_url');
$token = $this->config->get('webhook_token'); $token = $this->config->get('webhook_token');
if (strpos($url, '?') !== false) { if (! empty($url)) {
$url .= '&token='.$token;
}
else {
$url .= '?token='.$token;
}
return $this->httpClient->post($url, $task); if (strpos($url, '?') !== false) {
$url .= '&token='.$token;
}
else {
$url .= '?token='.$token;
}
return $this->httpClient->post($url, $values);
}
} }
} }

View File

@@ -6,7 +6,16 @@ use PDO;
use Core\Security; use Core\Security;
use Model\Link; use Model\Link;
const VERSION = 70; const VERSION = 71;
function version_71($pdo)
{
$rq = $pdo->prepare('INSERT INTO `settings` VALUES (?, ?)');
$rq->execute(array('webhook_url', ''));
$pdo->exec("DELETE FROM `settings` WHERE `option`='webhook_url_task_creation'");
$pdo->exec("DELETE FROM `settings` WHERE `option`='webhook_url_task_modification'");
}
function version_70($pdo) function version_70($pdo)
{ {

View File

@@ -6,7 +6,16 @@ use PDO;
use Core\Security; use Core\Security;
use Model\Link; use Model\Link;
const VERSION = 51; const VERSION = 52;
function version_52($pdo)
{
$rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
$rq->execute(array('webhook_url', ''));
$pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_creation'");
$pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_modification'");
}
function version_51($pdo) function version_51($pdo)
{ {

View File

@@ -6,7 +6,16 @@ use Core\Security;
use PDO; use PDO;
use Model\Link; use Model\Link;
const VERSION = 69; const VERSION = 70;
function version_70($pdo)
{
$rq = $pdo->prepare('INSERT INTO settings VALUES (?, ?)');
$rq->execute(array('webhook_url', ''));
$pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_creation'");
$pdo->exec("DELETE FROM settings WHERE option='webhook_url_task_modification'");
}
function version_69($pdo) function version_69($pdo)
{ {

View File

@@ -2,8 +2,13 @@
namespace Subscriber; namespace Subscriber;
use Event\CommentEvent;
use Event\GenericEvent;
use Event\TaskEvent; use Event\TaskEvent;
use Model\Comment;
use Model\Task; use Model\Task;
use Model\File;
use Model\Subtask;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class WebhookSubscriber extends Base implements EventSubscriberInterface class WebhookSubscriber extends Base implements EventSubscriberInterface
@@ -11,32 +16,30 @@ class WebhookSubscriber extends Base implements EventSubscriberInterface
public static function getSubscribedEvents() public static function getSubscribedEvents()
{ {
return array( return array(
Task::EVENT_CREATE => array('onTaskCreation', 0), Task::EVENT_CREATE => array('execute', 0),
Task::EVENT_UPDATE => array('onTaskModification', 0), Task::EVENT_UPDATE => array('execute', 0),
Task::EVENT_CLOSE => array('onTaskModification', 0), Task::EVENT_CLOSE => array('execute', 0),
Task::EVENT_OPEN => array('onTaskModification', 0), Task::EVENT_OPEN => array('execute', 0),
Task::EVENT_MOVE_COLUMN => array('onTaskModification', 0), Task::EVENT_MOVE_COLUMN => array('execute', 0),
Task::EVENT_MOVE_POSITION => array('onTaskModification', 0), Task::EVENT_MOVE_POSITION => array('execute', 0),
Task::EVENT_ASSIGNEE_CHANGE => array('onTaskModification', 0), Task::EVENT_ASSIGNEE_CHANGE => array('execute', 0),
Task::EVENT_MOVE_PROJECT => array('execute', 0),
Task::EVENT_MOVE_SWIMLANE => array('execute', 0),
Comment::EVENT_CREATE => array('execute', 0),
Comment::EVENT_UPDATE => array('execute', 0),
File::EVENT_CREATE => array('execute', 0),
Subtask::EVENT_CREATE => array('execute', 0),
Subtask::EVENT_UPDATE => array('execute', 0),
); );
} }
public function onTaskCreation(TaskEvent $event) public function execute(GenericEvent $event, $event_name)
{ {
$this->executeRequest('webhook_url_task_creation', $event); $payload = array(
} 'event_name' => $event_name,
'event_data' => $event->getAll(),
);
public function onTaskModification(TaskEvent $event) $this->webhook->notify($payload);
{
$this->executeRequest('webhook_url_task_modification', $event);
}
public function executeRequest($parameter, TaskEvent $event)
{
$url = $this->config->get($parameter);
if (! empty($url)) {
$this->webhook->notify($url, $event->getAll());
}
} }
} }

View File

@@ -7,6 +7,7 @@
<?= $this->formCsrf() ?> <?= $this->formCsrf() ?>
<?= $this->formHidden('id', $values) ?> <?= $this->formHidden('id', $values) ?>
<?= $this->formHidden('task_id', $values) ?> <?= $this->formHidden('task_id', $values) ?>
<?= $this->formHidden('user_id', $values) ?>
<div class="form-tabs"> <div class="form-tabs">
<ul class="form-tabs-nav"> <ul class="form-tabs-nav">

View File

@@ -6,11 +6,8 @@
<?= $this->formCsrf() ?> <?= $this->formCsrf() ?>
<?= $this->formLabel(t('Webhook URL for task creation'), 'webhook_url_task_creation') ?> <?= $this->formLabel(t('Webhook URL'), 'webhook_url') ?>
<?= $this->formText('webhook_url_task_creation', $values, $errors) ?><br/> <?= $this->formText('webhook_url', $values, $errors) ?>
<?= $this->formLabel(t('Webhook URL for task modification'), 'webhook_url_task_modification') ?>
<?= $this->formText('webhook_url_task_modification', $values, $errors) ?><br/>
<div class="form-actions"> <div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>

View File

@@ -33,7 +33,7 @@
<div class="listing"> <div class="listing">
<ul class="no-bullet"> <ul class="no-bullet">
<li><strong><i class="fa fa-calendar"></i> <?= $this->a(t('iCalendar (iCal format, *.ics)'), 'ical', 'user', array('token' => $user['token']), false, '', '', true) ?></strong></li> <li><strong><i class="fa fa-calendar"></i> <?= $this->a(t('iCal feed'), 'ical', 'user', array('token' => $user['token']), false, '', '', true) ?></strong></li>
</ul> </ul>
</div> </div>
<?php endif ?> <?php endif ?>

View File

@@ -3,8 +3,269 @@ Webhooks
Webhooks are useful to perform actions with external applications. 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) - Webhooks can be used to create a task by calling a simple URL (You can also do that with the API)
- An external URL can be called automatically when a task is created or modified - An external URL can be called automatically when an event occurs in Kanboard (task creation, comment updated, etc)
How to write a webhook receiver?
--------------------------------
All internal events of Kanboard can be sent to an external URL.
- The webhook url have to be defined in **Settings > Webhooks > Webhook URL**.
- When an event is triggered Kanboard call automatically the predefined URL
- The data are encoded in JSON format and sent with a POST HTTP request
- The webhook token is also sent as a query string parameter, so you can check if the request really come from Kanboard.
- **Your custom URL must answer in less than 1 second**, those requests are synchronous (PHP limitation) and that can slow down the user interface if your script is too slow!
### List of supported events
- comment.create
- comment.update
- file.create
- task.move.project
- task.move.column
- task.move.position
- task.move.swimlane
- task.update
- task.create
- task.close
- task.open
- task.assignee_change
- subtask.update
- subtask.create
### Example of HTTP request
```
POST https://your_webhook_url/?token=WEBHOOK_TOKEN_HERE
User-Agent: Kanboard Webhook
Content-Type: application/json
Connection: close
{
"event_name": "task.move.column",
"event_data": {
"task_id": "1",
"project_id": "1",
"position": 1,
"column_id": "1",
"swimlane_id": "0",
"src_column_id": "2",
"dst_column_id": "1",
"date_moved": "1431991532",
"recurrence_status": "0",
"recurrence_trigger": "0"
}
}
```
All event payloads are in the following format:
```json
{
"event_name": "model.event_name",
"event_data": {
"key1": "value1",
"key2": "value2",
...
}
}
```
The `event_data` values are not necessary normalized across events.
### Examples of event payloads
Task creation:
```json
{
"event_name": "task.create",
"event_data": {
"title": "Demo",
"description": "",
"project_id": "1",
"owner_id": "1",
"category_id": 0,
"swimlane_id": 0,
"column_id": "2",
"color_id": "yellow",
"score": 0,
"time_estimated": 0,
"date_due": 0,
"creator_id": 1,
"date_creation": 1431991532,
"date_modification": 1431991532,
"date_moved": 1431991532,
"position": 1,
"task_id": 1
}
}
```
Task modification:
```json
{
"event_name": "task.update",
"event_data": {
"id": "1",
"title": "Demo",
"description": "",
"date_creation": "1431991532",
"color_id": "yellow",
"project_id": "1",
"column_id": "1",
"owner_id": "1",
"position": "1",
"is_active": "1",
"date_completed": null,
"score": "0",
"date_due": "0",
"category_id": "0",
"creator_id": "1",
"date_modification": 1431991603,
"reference": "",
"date_started": 1431993600,
"time_spent": 0,
"time_estimated": 0,
"swimlane_id": "0",
"date_moved": "1431991572",
"recurrence_status": "0",
"recurrence_trigger": "0",
"recurrence_factor": "0",
"recurrence_timeframe": "0",
"recurrence_basedate": "0",
"recurrence_parent": null,
"recurrence_child": null,
"task_id": "1"
}
}
```
Move a task to another column:
```json
{
"event_name": "task.move.column",
"event_data": {
"task_id": "1",
"project_id": "1",
"position": 1,
"column_id": "1",
"swimlane_id": "0",
"src_column_id": "2",
"dst_column_id": "1",
"date_moved": "1431991532",
"recurrence_status": "0",
"recurrence_trigger": "0"
}
}
```
Move a task to another position:
```json
{
"event_name": "task.move.position",
"event_data": {
"task_id": "2",
"project_id": "1",
"position": 1,
"column_id": "1",
"swimlane_id": "0",
"src_column_id": "1",
"dst_column_id": "1",
"date_moved": "1431996905",
"recurrence_status": "0",
"recurrence_trigger": "0"
}
}
```
Comment creation:
```json
{
"event_name": "comment.create",
"event_data": {
"id": 1,
"task_id": "1",
"user_id": "1",
"comment": "test",
"date": 1431991615
}
}
```
Comment modification:
```
{
"event_name": "comment.update",
"event_data": {
"id": "1",
"task_id": "1",
"user_id": "1",
"comment": "test edit"
}
}
```
Subtask creation:
```json
{
"event_name": "subtask.create",
"event_data": {
"id": 3,
"task_id": "1",
"title": "Test",
"user_id": "1",
"time_estimated": "2",
"position": 3
}
}
```
Subtask modification:
```json
{
"event_name": "subtask.update",
"event_data": {
"id": "1",
"status": 1,
"task_id": "1"
}
}
```
File upload:
```json
{
"event_name": "file.create",
"event_data": {
"task_id": "1",
"name": "test.png"
}
}
```
Screenshot created:
```json
{
"event_name": "file.create",
"event_data": {
"task_id": "2",
"name": "Screenshot taken May 19, 2015 at 10:56 AM"
}
}
```
Note: Webhooks configuration and payload have changed since Kanboard >= 1.0.15
How to create a task with a webhook? How to create a task with a webhook?
------------------------------------ ------------------------------------
@@ -38,61 +299,3 @@ Base URL: `http://YOUR_SERVER_HOSTNAME/?controller=webhook&action=task`
- `column_id`: Column on the board (Get the column id from the projects page, mouse over on the column name) - `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. 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

@@ -11,6 +11,35 @@ use SimpleLogger\File;
date_default_timezone_set('UTC'); date_default_timezone_set('UTC');
class FakeHttpClient
{
private $url = '';
private $data = array();
public function getUrl()
{
return $this->url;
}
public function getData()
{
return $this->data;
}
public function toPrettyJson()
{
return json_encode($this->data, JSON_PRETTY_PRINT);
}
public function post($url, array $data)
{
$this->url = $url;
$this->data = $data;
//echo $this->toPrettyJson();
return true;
}
}
abstract class Base extends PHPUnit_Framework_TestCase abstract class Base extends PHPUnit_Framework_TestCase
{ {
protected $container; protected $container;
@@ -43,6 +72,7 @@ abstract class Base extends PHPUnit_Framework_TestCase
$this->container['logger'] = new Logger; $this->container['logger'] = new Logger;
$this->container['logger']->setLogger(new File('/dev/null')); $this->container['logger']->setLogger(new File('/dev/null'));
$this->container['httpClient'] = new FakeHttpClient;
} }
public function tearDown() public function tearDown()

View File

@@ -138,15 +138,15 @@ class TimetableTest extends Base
$this->assertNotFalse($w->create(1, 2, '09:30', '12:00')); $this->assertNotFalse($w->create(1, 2, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 2, '13:00', '17:00')); $this->assertNotFalse($w->create(1, 2, '13:00', '17:00'));
$monday = new DateTime('Monday'); $monday = new DateTime('next Monday');
$tuesday = new DateTime('Tuesday'); $tuesday = new DateTime('next Monday + 1 day');
$timetable = $t->calculate(1, new DateTime('Monday'), new DateTime('Monday + 6 days')); $timetable = $t->calculate(1, new DateTime('Monday'), new DateTime('Monday + 6 days'));
$this->assertNotEmpty($timetable); $this->assertNotEmpty($timetable);
$this->assertCount(4, $timetable); $this->assertCount(4, $timetable);
// Start to work before timetable // Start to work before timetable
$date = new DateTime('Monday'); $date = clone($monday);
$date->setTime(5, 02); $date->setTime(5, 02);
$slot = $t->findClosestTimeSlot($date, $timetable); $slot = $t->findClosestTimeSlot($date, $timetable);
@@ -155,7 +155,7 @@ class TimetableTest extends Base
$this->assertEquals($monday->format('Y-m-d').' 12:00', $slot[1]->format('Y-m-d H:i')); $this->assertEquals($monday->format('Y-m-d').' 12:00', $slot[1]->format('Y-m-d H:i'));
// Start to work at the end of the timeslot // Start to work at the end of the timeslot
$date = new DateTime('Monday'); $date = clone($monday);
$date->setTime(12, 02); $date->setTime(12, 02);
$slot = $t->findClosestTimeSlot($date, $timetable); $slot = $t->findClosestTimeSlot($date, $timetable);
@@ -164,7 +164,7 @@ class TimetableTest extends Base
$this->assertEquals($monday->format('Y-m-d').' 12:00', $slot[1]->format('Y-m-d H:i')); $this->assertEquals($monday->format('Y-m-d').' 12:00', $slot[1]->format('Y-m-d H:i'));
// Start to work at lunch time // Start to work at lunch time
$date = new DateTime('Monday'); $date = clone($monday);
$date->setTime(12, 32); $date->setTime(12, 32);
$slot = $t->findClosestTimeSlot($date, $timetable); $slot = $t->findClosestTimeSlot($date, $timetable);
@@ -173,7 +173,7 @@ class TimetableTest extends Base
$this->assertEquals($monday->format('Y-m-d').' 17:00', $slot[1]->format('Y-m-d H:i')); $this->assertEquals($monday->format('Y-m-d').' 17:00', $slot[1]->format('Y-m-d H:i'));
// Start to work early in the morning // Start to work early in the morning
$date = new DateTime('Tuesday'); $date = clone($tuesday);
$date->setTime(8, 02); $date->setTime(8, 02);
$slot = $t->findClosestTimeSlot($date, $timetable); $slot = $t->findClosestTimeSlot($date, $timetable);
@@ -192,47 +192,50 @@ class TimetableTest extends Base
$this->assertNotFalse($w->create(1, 2, '09:30', '12:00')); $this->assertNotFalse($w->create(1, 2, '09:30', '12:00'));
$this->assertNotFalse($w->create(1, 2, '13:00', '17:00')); $this->assertNotFalse($w->create(1, 2, '13:00', '17:00'));
$monday = new DateTime('next Monday');
$tuesday = new DateTime('next Monday + 1 day');
// Different day // Different day
$start = new DateTime('Monday'); $start = clone($monday);
$start->setTime(16, 02); $start->setTime(16, 02);
$end = new DateTime('Tuesday'); $end = clone($tuesday);
$end->setTime(10, 03); $end->setTime(10, 03);
$this->assertEquals(1.5, $t->calculateEffectiveDuration(1, $start, $end)); $this->assertEquals(1.5, $t->calculateEffectiveDuration(1, $start, $end));
// Same time slot // Same time slot
$start = new DateTime('Monday'); $start = clone($monday);
$start->setTime(16, 02); $start->setTime(16, 02);
$end = new DateTime('Monday'); $end = clone($monday);
$end->setTime(17, 03); $end->setTime(17, 03);
$this->assertEquals(1, $t->calculateEffectiveDuration(1, $start, $end)); $this->assertEquals(1, $t->calculateEffectiveDuration(1, $start, $end));
// Intermediate time slot // Intermediate time slot
$start = new DateTime('Monday'); $start = clone($monday);
$start->setTime(10, 02); $start->setTime(10, 02);
$end = new DateTime('Tuesday'); $end = clone($tuesday);
$end->setTime(16, 03); $end->setTime(16, 03);
$this->assertEquals(11.5, $t->calculateEffectiveDuration(1, $start, $end)); $this->assertEquals(11.5, $t->calculateEffectiveDuration(1, $start, $end));
// Different day // Different day
$start = new DateTime('Monday'); $start = clone($monday);
$start->setTime(9, 02); $start->setTime(9, 02);
$end = new DateTime('Tuesday'); $end = clone($tuesday);
$end->setTime(10, 03); $end->setTime(10, 03);
$this->assertEquals(7, $t->calculateEffectiveDuration(1, $start, $end)); $this->assertEquals(7, $t->calculateEffectiveDuration(1, $start, $end));
// Start before first time slot // Start before first time slot
$start = new DateTime('Monday'); $start = clone($monday);
$start->setTime(5, 32); $start->setTime(5, 32);
$end = new DateTime('Tuesday'); $end = clone($tuesday);
$end->setTime(11, 17); $end->setTime(11, 17);
$this->assertEquals(8.25, $t->calculateEffectiveDuration(1, $start, $end)); $this->assertEquals(8.25, $t->calculateEffectiveDuration(1, $start, $end));
@@ -242,10 +245,10 @@ class TimetableTest extends Base
{ {
$t = new Timetable($this->container); $t = new Timetable($this->container);
$start = new DateTime('Monday'); $start = new DateTime('next Monday');
$start->setTime(16, 02); $start->setTime(16, 02);
$end = new DateTime('Monday'); $end = new DateTime('next Monday');
$end->setTime(17, 03); $end->setTime(17, 03);
$this->assertEquals(1, $t->calculateEffectiveDuration(1, $start, $end)); $this->assertEquals(1, $t->calculateEffectiveDuration(1, $start, $end));

112
tests/units/WebhookTest.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
require_once __DIR__.'/Base.php';
use Model\Config;
use Model\Task;
use Model\TaskCreation;
use Model\TaskModification;
use Model\Project;
use Model\Comment;
use Subscriber\WebhookSubscriber;
class WebhookTest extends Base
{
public function testTaskCreation()
{
$c = new Config($this->container);
$p = new Project($this->container);
$tc = new TaskCreation($this->container);
$this->container['dispatcher']->addSubscriber(new WebhookSubscriber($this->container));
$c->save(array('webhook_url' => 'http://localhost/?task-creation'));
$this->assertEquals(1, $p->create(array('name' => 'test')));
$this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test')));
$this->assertStringStartsWith('http://localhost/?task-creation&token=', $this->container['httpClient']->getUrl());
$event = $this->container['httpClient']->getData();
$this->assertNotEmpty($event);
$this->assertArrayHasKey('event_name', $event);
$this->assertArrayHasKey('event_data', $event);
$this->assertEquals('task.create', $event['event_name']);
$this->assertNotEmpty($event['event_data']);
$this->assertArrayHasKey('project_id', $event['event_data']);
$this->assertArrayHasKey('task_id', $event['event_data']);
$this->assertArrayHasKey('title', $event['event_data']);
$this->assertArrayHasKey('column_id', $event['event_data']);
$this->assertArrayHasKey('color_id', $event['event_data']);
$this->assertArrayHasKey('swimlane_id', $event['event_data']);
$this->assertArrayHasKey('date_creation', $event['event_data']);
$this->assertArrayHasKey('date_modification', $event['event_data']);
$this->assertArrayHasKey('date_moved', $event['event_data']);
$this->assertArrayHasKey('position', $event['event_data']);
}
public function testTaskModification()
{
$c = new Config($this->container);
$p = new Project($this->container);
$tc = new TaskCreation($this->container);
$tm = new TaskModification($this->container);
$this->container['dispatcher']->addSubscriber(new WebhookSubscriber($this->container));
$c->save(array('webhook_url' => 'http://localhost/modif/'));
$this->assertEquals(1, $p->create(array('name' => 'test')));
$this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test')));
$this->assertTrue($tm->update(array('id' => 1, 'title' => 'test update')));
$this->assertStringStartsWith('http://localhost/modif/?token=', $this->container['httpClient']->getUrl());
$event = $this->container['httpClient']->getData();
$this->assertNotEmpty($event);
$this->assertArrayHasKey('event_name', $event);
$this->assertArrayHasKey('event_data', $event);
$this->assertEquals('task.update', $event['event_name']);
$this->assertNotEmpty($event['event_data']);
$this->assertArrayHasKey('project_id', $event['event_data']);
$this->assertArrayHasKey('task_id', $event['event_data']);
$this->assertArrayHasKey('title', $event['event_data']);
$this->assertArrayHasKey('column_id', $event['event_data']);
$this->assertArrayHasKey('color_id', $event['event_data']);
$this->assertArrayHasKey('swimlane_id', $event['event_data']);
$this->assertArrayHasKey('date_creation', $event['event_data']);
$this->assertArrayHasKey('date_modification', $event['event_data']);
$this->assertArrayHasKey('date_moved', $event['event_data']);
$this->assertArrayHasKey('position', $event['event_data']);
}
public function testCommentCreation()
{
$c = new Config($this->container);
$p = new Project($this->container);
$tc = new TaskCreation($this->container);
$cm = new Comment($this->container);
$this->container['dispatcher']->addSubscriber(new WebhookSubscriber($this->container));
$c->save(array('webhook_url' => 'http://localhost/comment'));
$this->assertEquals(1, $p->create(array('name' => 'test')));
$this->assertEquals(1, $tc->create(array('project_id' => 1, 'title' => 'test')));
$this->assertEquals(1, $cm->create(array('task_id' => 1, 'comment' => 'test comment', 'user_id' => 1)));
$this->assertStringStartsWith('http://localhost/comment?token=', $this->container['httpClient']->getUrl());
$event = $this->container['httpClient']->getData();
$this->assertNotEmpty($event);
$this->assertArrayHasKey('event_name', $event);
$this->assertArrayHasKey('event_data', $event);
$this->assertEquals('comment.create', $event['event_name']);
$this->assertNotEmpty($event['event_data']);
$this->assertArrayHasKey('task_id', $event['event_data']);
$this->assertArrayHasKey('user_id', $event['event_data']);
$this->assertArrayHasKey('comment', $event['event_data']);
$this->assertArrayHasKey('id', $event['event_data']);
$this->assertEquals('test comment', $event['event_data']['comment']);
}
}