├── CONTRIBUTING.md ├── README.md ├── UPGRADE.md ├── composer.json ├── spark.md └── src ├── Mpociot └── CaptainHook │ ├── CaptainHookServiceProvider.php │ ├── Commands │ ├── AddWebhook.php │ ├── DeleteWebhook.php │ └── ListWebhooks.php │ ├── Http │ ├── Requests │ │ ├── CreateWebhookRequest.php │ │ └── UpdateWebhookRequest.php │ ├── WebhookController.php │ └── WebhookEventsController.php │ ├── Jobs │ └── TriggerWebhooksJob.php │ ├── Webhook.php │ └── WebhookLog.php ├── config └── config.php ├── database ├── 2015_10_29_000000_captain_hook_setup_table.php └── 2015_10_29_000001_captain_hook_setup_logs_table.php ├── resources ├── assets │ └── js │ │ └── captainhook │ │ ├── settings │ │ ├── create-webhook.js │ │ └── webhooks.js │ │ └── webhooks.js └── views │ ├── webhooks.blade.php │ └── webhooks │ ├── create-webhook.blade.php │ └── webhooks.blade.php └── routes.php /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Captain Hook 2 | # Captain Hook 3 | ## Add Webhooks to your Laravel app, arrr 4 | 5 | ![image](http://img.shields.io/packagist/v/mpociot/captainhook.svg?style=flat) 6 | ![image](http://img.shields.io/packagist/l/mpociot/captainhook.svg?style=flat) 7 | [![codecov.io](https://codecov.io/github/mpociot/captainhook/coverage.svg?branch=master)](https://codecov.io/github/mpociot/captainhook?branch=master) 8 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/mpociot/captainhook/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/mpociot/captainhook/?branch=master) 9 | [![Build Status](https://travis-ci.org/mpociot/captainhook.svg?branch=master)](https://travis-ci.org/mpociot/captainhook) 10 | [![StyleCI](https://styleci.io/repos/45216255/shield)](https://styleci.io/repos/45216255) 11 | 12 | Implement multiple webhooks into your Laravel app using the Laravel Event system. 13 | 14 | ``` 15 | A webhook is a method of altering the behavior of a web application, with custom callbacks. 16 | These callbacks may be managed by third-party users and developers who may not necessarily 17 | be affiliated with the originating application. 18 | ``` 19 | 20 | ## Examples 21 | 22 | ```bash 23 | php artisan hook:add http://www.myapp.com/hooks/ '\App\Events\PodcastWasPurchased' 24 | php artisan hook:add http://www.myapp.com/hooks/ 'eloquent.saved: \App\User' 25 | ``` 26 | 27 | ```php 28 | Webhook::create([ 29 | "url" => Input::get("url"), 30 | "event" => "\\App\\Events\\MyEvent", 31 | "tenant_id" => Auth::id() 32 | ]); 33 | ``` 34 | 35 | ## Contents 36 | 37 | - [Installation](#installation) 38 | - [Implementation](#implementation) 39 | - [Usage](#usage) 40 | - [Spark support](#spark) 41 | - [Custom event listeners](#listeners) 42 | - [Add new webhooks](#add) 43 | - [Delete existing webhooks](#delete) 44 | - [List all active webhooks](#list) 45 | - [Receiving a webhook notification](#webhook) 46 | - [Webhook logging](#logging) 47 | - [Using webhooks with multi tenancy](#tenant) 48 | - [License](#license) 49 | 50 | 51 | ## Installation 52 | 53 | In order to add CaptainHook to your project, just add 54 | 55 | "mpociot/captainhook": "~2.0" 56 | 57 | to your `composer.json`'s `require` block. Then run `composer install` or `composer update`. 58 | 59 | Or run `composer require mpociot/captainhook ` if you prefer that. 60 | 61 | Then in your `config/app.php` add 62 | 63 | Mpociot\CaptainHook\CaptainHookServiceProvider::class 64 | 65 | to the `providers` array. 66 | 67 | 68 | Publish and run the migration to create the "webhooks" table that will hold all installed webhooks. 69 | 70 | ```bash 71 | php artisan vendor:publish --provider="Mpociot\CaptainHook\CaptainHookServiceProvider" 72 | 73 | php artisan migrate 74 | ``` 75 | 76 | 77 | ## Usage 78 | 79 | The CaptainHook service provider listens for any `eloquent.*` events. 80 | 81 | If the package finds a configured webhook for an event, it will make a `POST` request to the specified URL. 82 | 83 | Webhook data is sent as JSON in the POST request body. The full event object is included and can be used directly, after parsing the JSON body. 84 | 85 | **Example** 86 | 87 | Let's say you want to have a webhook that gets called every time your User model gets updated. 88 | 89 | The event that gets called from Laravel will be: 90 | 91 | `eloquent.updated: \App\User` 92 | 93 | So this will be the event you want to listen for. 94 | 95 | 96 | ### Add new webhooks 97 | 98 | If you know which event you want to listen to, you can add a new webhook by using the `hook:add` artisan command. 99 | 100 | This command takes two arguments: 101 | 102 | - The webhook URL that will receive the POST requests 103 | - The event name. This could either be one of the `eloquent.*` events, or one of your custom events. 104 | 105 | ```bash 106 | php artisan hook:add http://www.myapp.com/hook/ 'eloquent.saved: \App\User' 107 | ``` 108 | 109 | You can also add multiple webhooks for the same event, as all configured webhooks will get called asynchronously. 110 | 111 | 112 | ### Delete existing webhooks 113 | 114 | To remove an existing webhook from the system, use the `hook:delete` command. This command takes the webhook ID as an argument. 115 | 116 | ```bash 117 | php artisan hook:delete 2 118 | ``` 119 | 120 | 121 | ### List all active webhooks 122 | 123 | To list all existing webhooks, use the `hook:list` command. 124 | 125 | It will output all configured webhooks in a table. 126 | 127 | 128 | ### Spark 129 | 130 | Install this package like stated in the [Installation section](#installation), then follow the [Spark installation instructions](spark.md). 131 | 132 | 133 | ### Custom event listeners 134 | 135 | All listeners are defined in the config file located at `config/captain_hook.php`. 136 | 137 | 138 | ### Receiving a webhook notification 139 | 140 | To receive the event data in your configured webhook, use: 141 | 142 | ```php 143 | // Retrieve the request's body and parse it as JSON 144 | $input = @file_get_contents("php://input"); 145 | $event_json = json_decode($input); 146 | 147 | // Do something with $event_json 148 | ``` 149 | 150 | 151 | ### Webhook logging 152 | 153 | Starting with version 2.0, this package allows you to log the payload and response of the triggered webhooks. 154 | 155 | > **NOTE:** A non-blocking queue driver (not `sync`) is highly recommended. Otherwise your application will need to wait for the webhook execution. 156 | 157 | You can configure how many logs will be saved **per webhook** (Default 50). 158 | 159 | This value can be modified in the configuration file `config/captain_hook.php`. 160 | 161 | 162 | ### Using webhooks with multi tenancy 163 | 164 | Sometimes you don't want to use system wide webhooks, but rather want them scoped to a specific "tenant". 165 | This could be bound to a user or a team. 166 | 167 | The webhook table has a field `tenant_id` for this purpose. 168 | So if you want your users to be able to add their own webhooks, you won't use the artisan commands to add webhooks to the database, 169 | but add them on your own. 170 | 171 | To add a webhook that is scoped to the current user, you could do for example: 172 | 173 | ```php 174 | Webhook::create([ 175 | "url" => Input::get("url"), 176 | "event" => "\\App\\Events\\MyEvent", 177 | "tenant_id" => Auth::id() 178 | ]); 179 | ``` 180 | 181 | Now when you fire this event, you want to call the webhook only for the currently logged in user. 182 | 183 | In order to filter the webhooks, modify the `filter` configuration value in the `config/captain_hook.php` file. 184 | This filter is a Laravel collection filter. 185 | 186 | To return only the webhooks for the currently logged in user, it might look like this: 187 | 188 | ```php 189 | 'filter' => function( $webhook ){ 190 | return $webhook->tenant_id == Auth::id(); 191 | }, 192 | ``` 193 | 194 | 195 | ## License 196 | 197 | CaptainHook is free software distributed under the terms of the MIT license. 198 | 199 | 'Day 02: Table, Lamp & Treasure Map' image licensed under [Creative Commons 2.0](https://creativecommons.org/licenses/by/2.0/) - Photo from [stevedave](https://www.flickr.com/photos/stevedave/4153323914) 200 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade Captain Hook from version 2.* to 3.* 2 | 3 | ## Breaking changes 4 | - The `listeners` property in the configuration file is now an associative array. The key can be used to display a meaningful event name (which will also be used for Spark). 5 | 6 | # Upgrade Captain Hook from version 1.* to 2.* 7 | 8 | ## Breaking changes 9 | - Custom event listeners are no longered declared in an extended Service Provider. Place your custom events in the `config/captain_hook.php` instead. 10 | - There's no longer the need to extend the Service Provider. 11 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mpociot/captainhook", 3 | "license": "MIT", 4 | "description": "Add webhooks to your Laravel app.", 5 | "keywords": [ 6 | "events", 7 | "hook", 8 | "laravel", 9 | "webhook", 10 | "webhooks" 11 | ], 12 | "homepage": "http://github.com/mpociot/captainhook", 13 | "authors": [ 14 | { 15 | "name": "Marcel Pociot", 16 | "email": "m.pociot@gmail.com" 17 | } 18 | ], 19 | "support": { 20 | "issues": "https://github.com/mpociot/captainhook/issues", 21 | "source": "https://github.com/mpociot/captainhook" 22 | }, 23 | "require": { 24 | "php": ">=5.4.0", 25 | "illuminate/support": "~5.0", 26 | "guzzlehttp/guzzle": "~6.0" 27 | }, 28 | "require-dev": { 29 | "illuminate/database": "~5.0", 30 | "illuminate/events": "~5.0", 31 | "mockery/mockery": "~0.9", 32 | "orchestra/testbench": "~3.0", 33 | "phpunit/phpunit": "~4.7 || ~5.0" 34 | }, 35 | "autoload": { 36 | "psr-0": { 37 | "Mpociot\\CaptainHook": "src/" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spark.md: -------------------------------------------------------------------------------- 1 | # Spark installation process 2 | 3 | This package comes with predefined views and routes to use with your existing Spark installation. 4 | 5 | 6 | 7 | In order to install Captainhook into your Spark application: 8 | 9 | **1. Publish the Spark resources (views, VueJS components):** 10 | 11 | `php artisan vendor:publish --provider="Mpociot\CaptainHook\CaptainHookServiceProvider" --tag="spark-resources"` 12 | 13 | **2. Add the javascript components to your bootstrap.js file** 14 | 15 | Add `require('./captainhook/webhooks.js');` to your `resources/assets/js/components/bootstrap.js` file. 16 | 17 | **3. Compile the Javascript components** 18 | 19 | `gulp` 20 | 21 | **4. Add the HTML snippets** 22 | 23 | File: `vendor/spark/settings.blade.php` 24 | 25 | Place a link to the webhooks settings tab: 26 | 27 | ```html 28 | 29 |
  • 30 | 31 | Webhooks 32 | 33 |
  • 34 | ``` 35 | 36 | Inside the `` section, place the code to load the webhooks tab: 37 | 38 | ```html 39 |
    40 | @include('captainhook::settings.webhooks') 41 |
    42 | ``` 43 | 44 | **5. Try it out** 45 | 46 | Log into your Spark application and access the new webhook tab located at: 47 | 48 | `http://your-spark.url/settings#/webhooks` 49 | 50 | **Important note:** 51 | 52 | To make sure that the webhooks only get called for the correct user, modify the 'filter' property of the `config/captain_hook.php` 53 | 54 | ```php 55 | 'filter' => function ($webhook) { 56 | return $webhook->tenant_id == auth()->user()->getKey(); 57 | }, 58 | ``` 59 | -------------------------------------------------------------------------------- /src/Mpociot/CaptainHook/CaptainHookServiceProvider.php: -------------------------------------------------------------------------------- 1 | client = new Client(); 59 | $this->cache = app('Illuminate\Contracts\Cache\Repository'); 60 | $this->config = app('Illuminate\Contracts\Config\Repository'); 61 | $this->publishMigration(); 62 | $this->publishConfig(); 63 | $this->publishSparkResources(); 64 | $this->listeners = collect($this->config->get('captain_hook.listeners', []))->values(); 65 | $this->registerEventListeners(); 66 | $this->registerRoutes(); 67 | } 68 | 69 | /** 70 | * Register the service provider. 71 | * 72 | * @return void 73 | */ 74 | public function register() 75 | { 76 | $this->registerCommands(); 77 | } 78 | 79 | /** 80 | * Publish migration. 81 | */ 82 | protected function publishMigration() 83 | { 84 | $migrations = [ 85 | __DIR__.'/../../database/2015_10_29_000000_captain_hook_setup_table.php' => 86 | database_path('/migrations/2015_10_29_000000_captain_hook_setup_table.php'), 87 | __DIR__.'/../../database/2015_10_29_000001_captain_hook_setup_logs_table.php' => 88 | database_path('/migrations/2015_10_29_000001_captain_hook_setup_logs_table.php'), 89 | ]; 90 | 91 | // To be backwards compatible 92 | foreach ($migrations as $migration => $toPath) { 93 | preg_match('/_captain_hook_.*\.php/', $migration, $match); 94 | $published_migration = glob(database_path('/migrations/*'.$match[0])); 95 | if (count($published_migration) !== 0) { 96 | unset($migrations[$migration]); 97 | } 98 | } 99 | 100 | $this->publishes($migrations, 'migrations'); 101 | } 102 | 103 | /** 104 | * Publish configuration file. 105 | */ 106 | protected function publishConfig() 107 | { 108 | $this->publishes([ 109 | __DIR__.'/../../config/config.php' => config_path('captain_hook.php'), 110 | ]); 111 | } 112 | 113 | protected function publishSparkResources() 114 | { 115 | $this->loadViewsFrom(__DIR__.'/../../resources/views/', 'captainhook'); 116 | 117 | $this->publishes([ 118 | __DIR__.'/../../resources/assets/js/' => base_path('resources/assets/js/components/'), 119 | __DIR__.'/../../resources/views/' => base_path('resources/views/vendor/captainhook/settings/'), 120 | ], 'spark-resources'); 121 | } 122 | 123 | /** 124 | * Register all active event listeners. 125 | */ 126 | protected function registerEventListeners() 127 | { 128 | foreach ($this->listeners as $eventName) { 129 | $this->app['events']->listen($eventName, [$this, 'handleEvent']); 130 | } 131 | } 132 | 133 | /** 134 | * @param array $listeners 135 | */ 136 | public function setListeners($listeners) 137 | { 138 | $this->listeners = $listeners; 139 | 140 | $this->registerEventListeners(); 141 | } 142 | 143 | /** 144 | * @param array $webhooks 145 | */ 146 | public function setWebhooks($webhooks) 147 | { 148 | $this->webhooks = $webhooks; 149 | $this->getCache()->rememberForever(Webhook::CACHE_KEY, function () { 150 | return $this->webhooks; 151 | }); 152 | } 153 | 154 | /** 155 | * @return \Illuminate\Support\Collection 156 | */ 157 | public function getWebhooks() 158 | { 159 | // Check if migration ran 160 | if (Schema::hasTable((new Webhook)->getTable())) { 161 | return collect($this->getCache()->rememberForever(Webhook::CACHE_KEY, function () { 162 | return Webhook::all(); 163 | })); 164 | } 165 | 166 | return collect(); 167 | } 168 | 169 | /** 170 | * @return \Illuminate\Contracts\Cache\Repository 171 | */ 172 | public function getCache() 173 | { 174 | return $this->cache; 175 | } 176 | 177 | /** 178 | * @param \Illuminate\Contracts\Cache\Repository $cache 179 | */ 180 | public function setCache($cache) 181 | { 182 | $this->cache = $cache; 183 | } 184 | 185 | /** 186 | * @param ClientInterface $client 187 | */ 188 | public function setClient($client) 189 | { 190 | $this->client = $client; 191 | } 192 | 193 | /** 194 | * @param \Illuminate\Contracts\Config\Repository $config 195 | */ 196 | public function setConfig($config) 197 | { 198 | $this->config = $config; 199 | } 200 | 201 | /** 202 | * Event listener. 203 | * 204 | * @param $eventData 205 | */ 206 | public function handleEvent($eventData) 207 | { 208 | $eventName = Event::firing(); 209 | $webhooks = $this->getWebhooks()->where('event', $eventName); 210 | $webhooks = $webhooks->filter($this->config->get('captain_hook.filter', null)); 211 | 212 | if (! $webhooks->isEmpty()) { 213 | $this->dispatch(new TriggerWebhooksJob($webhooks, $eventData)); 214 | } 215 | } 216 | 217 | /** 218 | * Register the artisan commands. 219 | */ 220 | protected function registerCommands() 221 | { 222 | $this->commands( 223 | ListWebhooks::class, 224 | AddWebhook::class, 225 | DeleteWebhook::class 226 | ); 227 | } 228 | 229 | /** 230 | * Register predefined routes used for Spark. 231 | */ 232 | protected function registerRoutes() 233 | { 234 | if (class_exists('Laravel\Spark\Providers\AppServiceProvider')) { 235 | include __DIR__.'/../../routes.php'; 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/Mpociot/CaptainHook/Commands/AddWebhook.php: -------------------------------------------------------------------------------- 1 | url = $this->argument('url'); 36 | $hook->event = $this->argument('event'); 37 | try { 38 | $hook->save(); 39 | $this->info('The webhook was saved successfully.'); 40 | $this->info('Event: '.$hook->event); 41 | $this->info('URL: '.$hook->url); 42 | } catch (Exception $e) { 43 | $this->error("The webhook couldn't be added to the database ".$e->getMessage()); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Mpociot/CaptainHook/Commands/DeleteWebhook.php: -------------------------------------------------------------------------------- 1 | argument('id'); 33 | $hook = Webhook::find($id); 34 | if ($hook === null) { 35 | $this->error('Webhook with ID '.$id.' could not be found.'); 36 | } else { 37 | $hook->delete(); 38 | $this->info('The webhook was deleted successfully.'); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Mpociot/CaptainHook/Commands/ListWebhooks.php: -------------------------------------------------------------------------------- 1 | get(); 32 | $this->table(['id', 'tenant_id', 'url', 'event'], $all->toArray()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Mpociot/CaptainHook/Http/Requests/CreateWebhookRequest.php: -------------------------------------------------------------------------------- 1 | 'required|url', 26 | 'event' => 'required', 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Mpociot/CaptainHook/Http/Requests/UpdateWebhookRequest.php: -------------------------------------------------------------------------------- 1 | 'required|url', 26 | 'event' => 'required', 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Mpociot/CaptainHook/Http/WebhookController.php: -------------------------------------------------------------------------------- 1 | getTenantId($request)) 30 | ->with('lastLog') 31 | ->with('logs') 32 | ->orderBy('created_at', 'desc') 33 | ->get(); 34 | } 35 | 36 | /** 37 | * Create a new webhook for the user. 38 | * 39 | * @param CreateWebhookRequest $request 40 | * @return Response 41 | */ 42 | public function store(CreateWebhookRequest $request) 43 | { 44 | $hook = Webhook::create([ 45 | 'url' => $request->url, 46 | 'tenant_id' => $this->getTenantId($request), 47 | 'event' => $request->event, 48 | ]); 49 | 50 | return response()->json($hook); 51 | } 52 | 53 | /** 54 | * Update the given webhook. 55 | * 56 | * @param UpdateWebhookRequest $request 57 | * @param string $webhookId 58 | * @return Response 59 | */ 60 | public function update(UpdateWebhookRequest $request, $webhookId) 61 | { 62 | $webhook = Webhook::where('tenant_id', $this->getTenantId($request)) 63 | ->where('id', $webhookId) 64 | ->firstOrFail(); 65 | 66 | $webhook->url = $request->url; 67 | $webhook->event = $request->event; 68 | $webhook->save(); 69 | } 70 | 71 | /** 72 | * Delete the given webhook. 73 | * 74 | * @param Request $request 75 | * @param string $webhookId 76 | * @return Response 77 | */ 78 | public function destroy(Request $request, $webhookId) 79 | { 80 | Webhook::where('tenant_id', $this->getTenantId($request)) 81 | ->where('id', $webhookId) 82 | ->firstOrFail() 83 | ->delete(); 84 | } 85 | 86 | protected function getTenantId(Request $request) 87 | { 88 | return (config('captain_hook.tenant_spark_model', 'User') == 'Team' && isset($request->user()->currentTeam)) ? $request->user()->currentTeam->id : $request->user()->getKey(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Mpociot/CaptainHook/Http/WebhookEventsController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 18 | } 19 | 20 | /** 21 | * Get all of the available webhook events. 22 | * 23 | * @return Response 24 | */ 25 | public function all(Request $request) 26 | { 27 | return collect(config('captain_hook.listeners', []))->transform(function ($key, $value) { 28 | return [ 29 | 'name' => $value, 30 | 'event' => $key, 31 | ]; 32 | })->values(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Mpociot/CaptainHook/Jobs/TriggerWebhooksJob.php: -------------------------------------------------------------------------------- 1 | eventData = $eventData; 44 | $this->webhooks = $webhooks; 45 | } 46 | 47 | /** 48 | * Resolves a string or callable to a valid callable. 49 | * @param string|callable $transformer 50 | * @param $defaultMethodName 51 | * @return callable 52 | */ 53 | private function resolveCallable($transformer, $defaultMethodName) 54 | { 55 | if (is_string($transformer)) { 56 | return function () use ($transformer, $defaultMethodName) { 57 | list($class, $method) = Str::parseCallback($transformer, $defaultMethodName); 58 | 59 | return call_user_func_array([app($class), $method], func_get_args()); 60 | }; 61 | } elseif (is_callable($transformer)) { 62 | return $transformer; 63 | } 64 | 65 | return function () { 66 | }; 67 | } 68 | 69 | /** 70 | * Execute the job. 71 | * 72 | * @return void 73 | */ 74 | public function handle() 75 | { 76 | $config = app('Illuminate\Contracts\Config\Repository'); 77 | $client = app(Client::class); 78 | 79 | if ($config->get('captain_hook.log.max_attempts', -1) !== -1 && $this->attempts() > $config->get('captain_hook.log.max_attempts')) { 80 | return; 81 | } 82 | 83 | $logging = $config->get('captain_hook.log.active'); 84 | $transformer = $this->resolveCallable($config->get('captain_hook.transformer'), 'transform'); 85 | $responseCallback = $this->resolveCallable($config->get('captain_hook.response_callback'), 'handle'); 86 | 87 | foreach ($this->webhooks as $webhook) { 88 | if ($logging) { 89 | if ($config->get('captain_hook.log.storage_quantity') != -1 && 90 | $webhook->logs()->count() >= $config->get('captain_hook.log.storage_quantity')) { 91 | $webhook->logs()->orderBy('updated_at', 'asc')->first()->delete(); 92 | } 93 | $log = new WebhookLog([ 94 | 'webhook_id' => $webhook['id'], 95 | 'url' => $webhook['url'], 96 | ]); 97 | $middleware = Middleware::tap(function (RequestInterface $request, $options) use ($log) { 98 | $log->payload_format = isset($request->getHeader('Content-Type')[0]) ? $request->getHeader('Content-Type')[0] : null; 99 | $log->payload = $request->getBody()->getContents(); 100 | }, function ($request, $options, PromiseInterface $response) use ($log, $webhook, $responseCallback) { 101 | $response->then(function (ResponseInterface $response) use ($log, $webhook, $responseCallback) { 102 | $log->status = $response->getStatusCode(); 103 | $log->response = $response->getBody()->getContents(); 104 | $log->response_format = $log->payload_format = isset($response->getHeader('Content-Type')[0]) ? $response->getHeader('Content-Type')[0] : null; 105 | 106 | $log->save(); 107 | 108 | // Retry this job if the webhook response didn't give us a HTTP 200 OK 109 | if ($response->getStatusCode() >= 300 || $response->getStatusCode() < 200) { 110 | $this->release(30); 111 | } 112 | 113 | $responseCallback($webhook, $response); 114 | }); 115 | }); 116 | 117 | $client->post($webhook['url'], [ 118 | 'exceptions' => false, 119 | 'body' => $transformer($this->eventData, $webhook), 120 | 'verify' => false, 121 | 'handler' => $middleware($client->getConfig('handler')), 122 | ]); 123 | } else { 124 | $client->post($webhook['url'], [ 125 | 'exceptions' => false, 126 | 'body' => $transformer($this->eventData, $webhook), 127 | 'verify' => false, 128 | 'timeout' => 10, 129 | ]); 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Mpociot/CaptainHook/Webhook.php: -------------------------------------------------------------------------------- 1 | hasMany(WebhookLog::class); 59 | } 60 | 61 | /** 62 | * Retrieve the logs for a given hook. 63 | * 64 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 65 | */ 66 | public function lastLog() 67 | { 68 | return $this->hasOne(WebhookLog::class)->orderBy('created_at', 'DESC'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Mpociot/CaptainHook/WebhookLog.php: -------------------------------------------------------------------------------- 1 | belongsTo(Webhook::class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'Eloquent' => 'eloquent.*', 22 | ], 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Webhook filter closure 27 | |-------------------------------------------------------------------------- 28 | | 29 | | If your webhooks are scoped to a tenant_id, you can modify 30 | | this filter function to return only the webhooks for your 31 | | tenant. This function is applied as a collection filter. 32 | | The tenant_id field can be used for verification. 33 | | 34 | */ 35 | 'filter' => function ($webhook) { 36 | return true; 37 | }, 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Webhook data transformer 42 | |-------------------------------------------------------------------------- 43 | | 44 | | The data transformer is a simple function that allows you to take the 45 | | subject data of an event and convert it to a format that will then 46 | | be posted to the webhooks. By default, all data is json encoded. 47 | | The second argument is the Webhook that was triggered in case 48 | | you want to transform the data in different ways per hook. 49 | | 50 | | You can also use the 'Foo\Class@transform' notation if you want. 51 | | 52 | */ 53 | 'transformer' => function ($eventData, $webhook) { 54 | return json_encode($eventData); 55 | }, 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Webhook response callback 60 | |-------------------------------------------------------------------------- 61 | | 62 | | The response callback can be used if you want to trigger 63 | | certain actions depending on the webhook response. 64 | | This is unused by default. 65 | | 66 | | You can also use the 'Foo\Class@handle' notation if you want. 67 | | 68 | */ 69 | 'response_callback' => function ($webhook, $response) { 70 | // Handle custom response status codes, ... 71 | }, 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Logging configuration 76 | |-------------------------------------------------------------------------- 77 | | 78 | | Captain Hook ships with built-in logging to allow you to store data 79 | | about the requests that you have made in a certain time interval. 80 | */ 81 | 'log' => [ 82 | 'active' => true, 83 | 'storage_quantity' => 50, 84 | 'max_attempts' => 5, 85 | ], 86 | 87 | /* 88 | |-------------------------------------------------------------------------- 89 | | Tenant configuration (Spark specific configuration) 90 | |-------------------------------------------------------------------------- 91 | | 92 | | The tenant model option allows you to associate the tenant_id 93 | | to the Spark Team instead of the User like by default. 94 | | 95 | | Possible options are: 'User' or 'Team' 96 | | 97 | | If you use 'User' you should add the following to the 'filter' function: 98 | | return $webhook->tenant_id == auth()->user()->getKey(); 99 | | 100 | | If you use 'Team' you should add the following to the 'filter' function: 101 | | return $webhook->tenant_id == auth()->user()->currentTeam->id; 102 | */ 103 | 'tenant_spark_model' => 'Team', 104 | 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | API configuration (Spark specific configuration) 108 | |-------------------------------------------------------------------------- 109 | | 110 | | By enabling this option some extra routes will be added under 111 | | the /api prefix and with the 'auth:api' middleware, to allow users and 112 | | services like Zapier to create, update and delete Webhooks without user 113 | | interaction. 114 | | See more at http://resthooks.org/ 115 | | 116 | */ 117 | 'uses_api' => true, 118 | ]; 119 | -------------------------------------------------------------------------------- /src/database/2015_10_29_000000_captain_hook_setup_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->integer('tenant_id')->unsigned()->nullable(); 18 | $table->string('url'); 19 | $table->string('event'); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::drop('webhooks'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/database/2015_10_29_000001_captain_hook_setup_logs_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->integer('webhook_id')->unsigned()->nullable(); 18 | $table->foreign('webhook_id')->references('id')->on('webhooks')->onDelete('set null'); 19 | $table->string('url'); 20 | $table->string('payload_format')->nullable(); 21 | $table->text('payload'); 22 | $table->integer('status'); 23 | $table->text('response'); 24 | $table->string('response_format')->nullable(); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::drop('webhook_logs'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/resources/assets/js/captainhook/settings/create-webhook.js: -------------------------------------------------------------------------------- 1 | Vue.component('captainhook-create-webhook', { 2 | props: ['availableEvents'], 3 | 4 | 5 | /** 6 | * The component's data. 7 | */ 8 | data() { 9 | return { 10 | allAbilitiesAssigned: false, 11 | 12 | form: new SparkForm({ 13 | name: '', 14 | event: null 15 | }) 16 | }; 17 | }, 18 | 19 | methods: { 20 | 21 | /** 22 | * Create a new webhook. 23 | */ 24 | create() { 25 | Spark.post('/settings/api/webhook', this.form) 26 | .then(response => { 27 | this.resetForm(); 28 | 29 | this.$dispatch('updateWebhooks'); 30 | }); 31 | }, 32 | 33 | 34 | /** 35 | * Reset the webhook form back to its default state. 36 | */ 37 | resetForm() { 38 | this.form.url = ''; 39 | this.form.event = ''; 40 | } 41 | } 42 | }); -------------------------------------------------------------------------------- /src/resources/assets/js/captainhook/settings/webhooks.js: -------------------------------------------------------------------------------- 1 | Vue.component('captainhook-webhooks', { 2 | 3 | props: ['webhooks', 'availableEvents'], 4 | 5 | 6 | /** 7 | * The component's data. 8 | */ 9 | data() { 10 | return { 11 | logForWebhook: null, 12 | updatingWebhook: null, 13 | deletingWebhook: null, 14 | inspectedLog: null, 15 | 16 | updateWebhookForm: new SparkForm({ 17 | url: '', 18 | event: '', 19 | }), 20 | 21 | deleteWebhookForm: new SparkForm({}) 22 | } 23 | }, 24 | 25 | filters: { 26 | readableName: function(event) { 27 | var readable = ''; 28 | this.availableEvents.forEach(function(value, key){ 29 | if(value.event === event) { 30 | readable = value.name; 31 | } 32 | }); 33 | return readable; 34 | } 35 | }, 36 | 37 | 38 | methods: { 39 | /** 40 | * Show the edit webhook modal. 41 | */ 42 | editWebhook(webhook) { 43 | this.updatingWebhook = webhook; 44 | 45 | this.initializeUpdateFormWith(webhook); 46 | 47 | $('#modal-update-webhook').modal('show'); 48 | }, 49 | 50 | 51 | /** 52 | * Show the edit webhook modal. 53 | */ 54 | showWebhookLogs(webhook) { 55 | this.logForWebhook = webhook; 56 | this.inspectedLog = null; 57 | 58 | $('#modal-webhook-logs').modal('show'); 59 | }, 60 | 61 | /** 62 | * 63 | */ 64 | toggleInspectLog(log) { 65 | if (this.inspectedLog && log === this.inspectedLog) { 66 | this.inspectedLog = null; 67 | } else { 68 | this.inspectedLog = log; 69 | } 70 | }, 71 | 72 | /** 73 | * Initialize the edit form with the given webhook. 74 | */ 75 | initializeUpdateFormWith(webhook) { 76 | this.updateWebhookForm.url = webhook.url; 77 | 78 | this.updateWebhookForm.event = webhook.event; 79 | }, 80 | 81 | 82 | /** 83 | * Update the webhook being edited. 84 | */ 85 | updateWebhook() { 86 | Spark.put(`/settings/api/webhook/${this.updatingWebhook.id}`, this.updateWebhookForm) 87 | .then(response => { 88 | this.$dispatch('updateWebhooks'); 89 | 90 | $('#modal-update-webhook').modal('hide'); 91 | }) 92 | }, 93 | 94 | /** 95 | * Get user confirmation that the webhook should be deleted. 96 | */ 97 | approveWebhookDelete(webhook) { 98 | this.deletingWebhook = webhook; 99 | 100 | $('#modal-delete-webhook').modal('show'); 101 | }, 102 | 103 | 104 | /** 105 | * Delete the specified webhook. 106 | */ 107 | deleteWebhook() { 108 | Spark.delete(`/settings/api/webhook/${this.deletingWebhook.id}`, this.deleteWebhookForm) 109 | .then(() => { 110 | this.$dispatch('updateWebhooks'); 111 | 112 | $('#modal-delete-webhook').modal('hide'); 113 | }); 114 | } 115 | } 116 | }); -------------------------------------------------------------------------------- /src/resources/assets/js/captainhook/webhooks.js: -------------------------------------------------------------------------------- 1 | require('./settings/webhooks'); 2 | require('./settings/create-webhook'); 3 | 4 | Vue.component('captainhook-webhook', { 5 | /** 6 | * The component's data. 7 | */ 8 | data() { 9 | return { 10 | webhooks: [], 11 | availableEvents: [] 12 | }; 13 | }, 14 | 15 | 16 | /** 17 | * Prepare the component. 18 | */ 19 | ready() { 20 | this.getWebhooks(); 21 | this.getAvailableEvents(); 22 | }, 23 | 24 | 25 | events: { 26 | /** 27 | * Broadcast that child components should update their webhooks. 28 | */ 29 | updateWebhooks() { 30 | this.getWebhooks(); 31 | } 32 | }, 33 | 34 | 35 | methods: { 36 | /** 37 | * Get the current API webhooks for the user. 38 | */ 39 | getWebhooks() { 40 | this.$http.get('/settings/api/webhooks') 41 | .then(function(response) { 42 | this.webhooks = response.data; 43 | }); 44 | }, 45 | 46 | 47 | /** 48 | * Get all of the available webhook events. 49 | */ 50 | getAvailableEvents() { 51 | this.$http.get('/settings/api/webhooks/events') 52 | .then(function(response) { 53 | this.availableEvents = response.data; 54 | }); 55 | } 56 | } 57 | }); 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/resources/views/webhooks.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 |
    5 | @include('captainhook::settings.webhooks.create-webhook') 6 |
    7 | 8 | 9 |
    10 | @include('captainhook::settings.webhooks.webhooks') 11 |
    12 |
    13 |
    14 | -------------------------------------------------------------------------------- /src/resources/views/webhooks/create-webhook.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 | Create Webhook 5 |
    6 | 7 |
    8 |
    9 | 10 |
    11 | 12 | 13 |
    14 | 15 | 16 | 17 | @{{ form.errors.get('url') }} 18 | 19 |
    20 |
    21 | 22 | 23 |
    24 | 25 | 26 |
    27 | 32 | 33 | 34 | @{{ form.errors.get('event') }} 35 | 36 |
    37 |
    38 | 39 | 40 |
    41 |
    42 | 48 |
    49 |
    50 |
    51 |
    52 |
    53 |
    54 | -------------------------------------------------------------------------------- /src/resources/views/webhooks/webhooks.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 |
    Webhooks
    5 | 6 |
    7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 31 | 32 | 33 | 44 | 45 | 46 | 51 | 52 | 53 | 58 | 59 | 60 | 65 | 66 | 67 |
    NameEventLast Response Code
    20 |
    21 | @{{ webhook.url }} 22 |
    23 |
    27 |
    28 | @{{ webhook.event | readableName }} 29 |
    30 |
    34 |
    35 | 36 | @{{ webhook.last_log.status }} 37 | 38 | 39 | 40 | Never called 41 | 42 |
    43 |
    47 | 50 | 54 | 57 | 61 | 64 |
    68 |
    69 |
    70 |
    71 | 72 | 73 | 130 | 131 | 132 | 159 | 160 | 161 | 230 |
    231 | -------------------------------------------------------------------------------- /src/routes.php: -------------------------------------------------------------------------------- 1 | ['web', 'auth']], function ($router) { 4 | $router->get('/settings/api/webhooks', 'Mpociot\CaptainHook\Http\WebhookController@all'); 5 | $router->post('/settings/api/webhook', 'Mpociot\CaptainHook\Http\WebhookController@store'); 6 | $router->put('/settings/api/webhook/{webhook_id}', 'Mpociot\CaptainHook\Http\WebhookController@update'); 7 | $router->delete('/settings/api/webhook/{webhook_id}', 'Mpociot\CaptainHook\Http\WebhookController@destroy'); 8 | 9 | $router->get('/settings/api/webhooks/events', 'Mpociot\CaptainHook\Http\WebhookEventsController@all'); 10 | }); 11 | 12 | if (config('captain_hook.uses_api', false)) { 13 | Route::group(['middleware' => 'auth:api', 'prefix' => 'api'], function ($router) { 14 | $router->get('webhooks', 'Mpociot\CaptainHook\Http\WebhookController@all'); 15 | $router->post('webhook', 'Mpociot\CaptainHook\Http\WebhookController@store'); 16 | $router->put('webhook/{webhook_id}', 'Mpociot\CaptainHook\Http\WebhookController@update'); 17 | $router->delete('webhook/{webhook_id}', 'Mpociot\CaptainHook\Http\WebhookController@destroy'); 18 | }); 19 | } 20 | --------------------------------------------------------------------------------