├── LICENSE ├── README.md ├── composer.json └── src ├── Handlers ├── BaseHandler.php ├── ComponentWasUpdatedHandler.php ├── IncidentWasReportedHandler.php └── IncidentWasUpdatedHandler.php ├── ServiceProvider.php └── resources ├── lang └── en │ └── messages.php └── slack.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ulrik Nielsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cachet Slack integration 2 | 3 | This package adds [Slack](https://slack.com) integration to your [Cachet](https://cachethq.io/) installation. 4 | 5 | When set up it will send notifications to a slack channel when a incident is either added or updated - and when 6 | components are added or updated. 7 | 8 | ## Install 9 | 10 | composer require mrbase/cachet-slack-integration 11 | 12 | Add provider to your config/app.php providers 13 | 14 | 'providers' => [ 15 | ... 16 | Maknz\Slack\SlackServiceProvider::class, 17 | Mrbase\CachetSlackIntegration\ServiceProvider::class, 18 | ], 19 | 20 | And to aliases: 21 | 22 | 'aliases' => [ 23 | ... 24 | 'Slack' => Maknz\Slack\Facades\Slack::class, 25 | ], 26 | 27 | Publish config and translations: 28 | 29 | php artisan vendor:publish 30 | 31 | ## Setup 32 | 33 | Edit `config/slack.php` and replace the following with your own settings, update any other settings as toy see fit: 34 | 35 | 'endpoint' => '', 36 | 37 | The endpoint url is something like: https://hooks.slack.com/services/XXXX/XXXX/XXX 38 | 39 | Remember to clear cache and config. 40 | 41 | Done, Cachet will now send notifications to your Slack channel on incident events. 42 | 43 | 44 | ## Note 45 | 46 | In the current version only one Slack account/channel is supported. I have plans for adding support for Slack subscriptions just as you subscribe to email notifications. 47 | 48 | 49 | Sponsored by [Unity](http://unity3d.com) 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mrbase/cachet-slack-integration", 3 | "description": "Adds Slack notifications on Cachet events.", 4 | "keywords": ["laravel", "cachet", "slack", "integration"], 5 | "homepage": "https://github.com/mrbase/cachet-slack-integration", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Ulrik Nielsen", 10 | "email": "me@ulrik.co", 11 | "homepage": "http://ulrik.co/" 12 | } 13 | ], 14 | "require": { 15 | "cachethq/cachet": "2.*", 16 | "maknz/slack-laravel": "^1.0" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "Mrbase\\CachetSlackIntegration\\": "src/" 21 | } 22 | }, 23 | "prefer-stable": true 24 | } 25 | -------------------------------------------------------------------------------- /src/Handlers/BaseHandler.php: -------------------------------------------------------------------------------- 1 | 'good', // 'Scheduled' 31 | 1 => 'danger', // 'Investigating' 32 | 2 => 'warning', // 'Identified' 33 | 3 => 'warning', // 'Watching' 34 | 4 => 'good', // 'Fixed' 35 | ]; 36 | 37 | if (isset($colormap[$status])) { 38 | return $colormap[$status]; 39 | } 40 | 41 | return ''; 42 | } 43 | 44 | /** 45 | * Find the status on a component. 46 | * 47 | * @param string $componentId 48 | * 49 | * @return string 50 | */ 51 | protected function getComponentStatus($componentId = '') 52 | { 53 | if ('' == $componentId) { 54 | return 'n/a'; 55 | } 56 | 57 | $statuses = trans('cachet.components.status'); 58 | $component = Component::find($componentId); 59 | 60 | if ($component instanceof Component) { 61 | return $component->name.': *'.$statuses[$component->status].'*'; 62 | } 63 | 64 | return 'n/a'; 65 | } 66 | 67 | /** 68 | * @param int $id 69 | * 70 | * @return string 71 | */ 72 | protected function translateIncidentStatus($id) 73 | { 74 | return trans('cachet.incidents.status')[$id]; 75 | } 76 | 77 | /** 78 | * @param int $id 79 | * 80 | * @return string 81 | */ 82 | protected function translateComponentStatus($id) 83 | { 84 | return trans('cachet.components.status')[$id]; 85 | } 86 | 87 | /** 88 | * Send Slack message as attachment. 89 | * 90 | * @param array $attachment 91 | * @param string $title 92 | * 93 | * @return mixed 94 | */ 95 | protected function sendAttachment($attachment, $title) 96 | { 97 | return Slack::attach($attachment)->send($title); 98 | } 99 | 100 | /** 101 | * Send plain Slack message. 102 | * 103 | * @param string $message 104 | * @return mixed 105 | */ 106 | protected function sendMessage($message) 107 | { 108 | return Slack::send($message); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Handlers/ComponentWasUpdatedHandler.php: -------------------------------------------------------------------------------- 1 | message = trans('slack::messages.component.status_update', [ 26 | 'name' => $name, 27 | 'status' => $this->translateComponentStatus($status), 28 | ]); 29 | } 30 | 31 | /** 32 | * Send the slack message. 33 | * 34 | * @return mixed 35 | */ 36 | public function send() 37 | { 38 | $this->sendMessage($this->message); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Handlers/IncidentWasReportedHandler.php: -------------------------------------------------------------------------------- 1 | $message, 29 | 'name' => $name, 30 | ]; 31 | 32 | $this->attachment = [ 33 | 'fallback' => trans('slack::messages.incident.created.fallback', $replacements), 34 | 'color' => $this->statusToColor($status), 35 | 'title' => trans('slack::messages.incident.created.title', $replacements), 36 | 'title_link' => route('status-page'), 37 | 'text' => $message, 38 | 'fields' => [ 39 | [ 40 | 'title' => trans('slack::messages.incident.field_labels.status'), 41 | 'value' => $this->translateIncidentStatus($status), 42 | 'short' => true, 43 | ], 44 | [ 45 | 'title' => trans('slack::messages.incident.field_labels.component'), 46 | 'value' => $this->getComponentStatus($componentId), 47 | 'short' => true, 48 | ], 49 | ], 50 | 'mrkdwn_in' => ['pretext', 'text', 'fields'] 51 | ]; 52 | } 53 | 54 | /** 55 | * Send the slack message. 56 | * 57 | * @return mixed 58 | */ 59 | public function send() 60 | { 61 | return $this->sendAttachment($this->attachment, trans('slack::messages.incident.created.header')); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Handlers/IncidentWasUpdatedHandler.php: -------------------------------------------------------------------------------- 1 | replacements = [ 47 | 'id' => $id, 48 | 'message' => $message, 49 | 'name' => $name, 50 | 'new_status' => $statuses[$newStatus], 51 | 'old_status' => $statuses[$oldStatus], 52 | 'state' => $state, 53 | ]; 54 | 55 | $message = isset($changes['message']) 56 | ? $changes['message'] 57 | : trans('slack::messages.incident.updated.text', $this->replacements); 58 | 59 | $this->attachment = [ 60 | 'fallback' => trans('slack::messages.incident.updated.fallback', $this->replacements), 61 | 'color' => $this->statusToColor($newStatus), 62 | 'title' => trans('slack::messages.incident.updated.title', $this->replacements), 63 | 'title_link' => route('status-page'), 64 | 'text' => $message, 65 | 'fields' => [ 66 | [ 67 | 'title' => trans('slack::messages.incident.field_labels.status'), 68 | 'value' => $this->translateIncidentStatus($status), 69 | 'short' => true, 70 | ], 71 | [ 72 | 'title' => trans('slack::messages.incident.field_labels.component'), 73 | 'value' => $this->getComponentStatus($componentId), 74 | 'short' => true, 75 | ], 76 | ], 77 | 'mrkdwn_in' => ['pretext', 'text', 'fields'] 78 | ]; 79 | } 80 | 81 | /** 82 | * Send the slack message. 83 | * 84 | * @return mixed 85 | */ 86 | public function send() 87 | { 88 | return $this->sendAttachment($this->attachment, trans('slack::messages.incident.updated.header', $this->replacements)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Mrbase\CachetSlackIntegration; 12 | 13 | use CachetHQ\Cachet\Bus\Events\Component\ComponentWasUpdatedEvent; 14 | use CachetHQ\Cachet\Bus\Events\Incident\IncidentWasReportedEvent; 15 | use CachetHQ\Cachet\Bus\Events\Incident\IncidentWasUpdatedEvent; 16 | use CachetHQ\Cachet\Models\Component; 17 | use CachetHQ\Cachet\Models\Incident; 18 | use Illuminate\Contracts\Events\Dispatcher as DispatcherContract; 19 | use Illuminate\Support\ServiceProvider as BaseServiceProvider; 20 | use Mrbase\CachetSlackIntegration\Handlers\ComponentWasUpdatedHandler; 21 | use Mrbase\CachetSlackIntegration\Handlers\IncidentWasReportedHandler; 22 | use Mrbase\CachetSlackIntegration\Handlers\IncidentWasUpdatedHandler; 23 | 24 | /** 25 | * Class ServiceProvider 26 | * 27 | * @package Mrbase\CachetSlackIntegration 28 | * @author Ulrik Nielsen 29 | */ 30 | class ServiceProvider extends BaseServiceProvider 31 | { 32 | /** 33 | * @var array 34 | */ 35 | private static $changes = []; 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function register(){} 41 | 42 | /** 43 | * @param DispatcherContract $events 44 | */ 45 | public function boot(DispatcherContract $events) 46 | { 47 | if (false === config('slack.endpoint', false)) { 48 | return; 49 | } 50 | 51 | $this->loadTranslationsFrom(__DIR__.'/resources/lang', 'slack'); 52 | 53 | /** 54 | * To get the actual changes we need to record the 55 | * changes before the Cachet event is fired. 56 | */ 57 | Incident::updating(function(Incident $incident) { 58 | $dirty = []; 59 | foreach ($incident->getDirty() as $key => $v) { 60 | $dirty[$key] = $incident->getOriginal($key); 61 | } 62 | self::registerChanges('incident', $dirty); 63 | }); 64 | 65 | Component::updating(function(Component $component) { 66 | $dirty = []; 67 | foreach ($component->getDirty() as $key => $v) { 68 | $dirty[$key] = $component->getOriginal($key); 69 | } 70 | self::registerChanges('component', $dirty); 71 | }); 72 | 73 | 74 | /** 75 | * Send Slack notification on new incidents. 76 | */ 77 | $events->listen('CachetHQ\Cachet\Bus\Events\Incident\IncidentWasReportedEvent', function (IncidentWasReportedEvent $event) { 78 | $handler = new IncidentWasReportedHandler( 79 | $event->incident->status, 80 | $event->incident->name, 81 | $event->incident->message, 82 | $event->incident['component_id'] 83 | ); 84 | 85 | return $handler->send(); 86 | }); 87 | 88 | 89 | /** 90 | * Send Slack notification on incident updates. 91 | */ 92 | $events->listen('CachetHQ\Cachet\Bus\Events\Incident\IncidentWasUpdatedEvent', function (IncidentWasUpdatedEvent $event) { 93 | $handler = new IncidentWasUpdatedHandler( 94 | $event->incident->id, 95 | $event->incident->status, 96 | $event->incident->name, 97 | $event->incident->message, 98 | $event->incident['component_id'], 99 | self::getChanges('incident') 100 | ); 101 | 102 | return $handler->send(); 103 | }); 104 | 105 | 106 | /** 107 | * Send Slack notification on component updates. 108 | * Note these are not send when a component is updated as part of an incident update. 109 | */ 110 | $events->listen('CachetHQ\Cachet\Bus\Events\Component\ComponentWasUpdatedEvent', function (ComponentWasUpdatedEvent $event) { 111 | $handler = new ComponentWasUpdatedHandler( 112 | $event->component->status, 113 | $event->component->name 114 | ); 115 | 116 | return $handler->send(); 117 | }); 118 | } 119 | 120 | /** 121 | * Register model object changes. 122 | * 123 | * @param string $model 124 | * @param array $data 125 | */ 126 | private static function registerChanges($model, array $data) 127 | { 128 | // For some reason, the changed/saving event is called twice on the model, we only need the first. 129 | if (!empty(self::$changes[$model])) { 130 | return; 131 | } 132 | 133 | self::$changes[$model] = $data; 134 | } 135 | 136 | /** 137 | * Return any changes to a model object. 138 | * 139 | * @param string $model 140 | * 141 | * @return array 142 | */ 143 | private static function getChanges($model) 144 | { 145 | if (isset(self::$changes[$model])) { 146 | return self::$changes[$model]; 147 | } 148 | 149 | return []; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/resources/lang/en/messages.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | return [ 12 | 'component' => [ 13 | 'status_update' => 'Component *:name* changed status to *:status*.' 14 | ], 15 | 16 | 'incident' => [ 17 | 'field_labels' => [ 18 | 'status' => 'Status', 19 | 'component' => 'Component', 20 | ], 21 | 'created' => [ 22 | 'fallback' => 'New incident: #:id :message', 23 | 'header' => 'New incident reported.', 24 | 'title' => ':name', 25 | ], 26 | 27 | 'updated' => [ 28 | 'fallback' => 'Incident: #:id :name - :state', 29 | 'header' => 'Incident: #:id :name - :state', 30 | 'title' => '#:id - :name', 31 | 'text' => 'Incident changed status from *:old_status* to *:new_status*:' 32 | ], 33 | ], 34 | ]; 35 | -------------------------------------------------------------------------------- /src/resources/slack.php: -------------------------------------------------------------------------------- 1 | env('SLACK_ENDPOINT', ''), 16 | 17 | /* 18 | |------------------------------------------------------------- 19 | | Default channel 20 | |------------------------------------------------------------- 21 | | 22 | | The default channel we should post to. The channel can either be a 23 | | channel like #general, a private #group, or a @username. Set to 24 | | null to use the default set on the Slack webhook 25 | | 26 | */ 27 | 28 | 'channel' => env('SLACK_CHANNEL', '#general'), 29 | 30 | /* 31 | |------------------------------------------------------------- 32 | | Default username 33 | |------------------------------------------------------------- 34 | | 35 | | The default username we should post as. Set to null to use 36 | | the default set on the Slack webhook 37 | | 38 | */ 39 | 40 | 'username' => env('SLACK_USERNAME', ''), 41 | 42 | /* 43 | |------------------------------------------------------------- 44 | | Default icon 45 | |------------------------------------------------------------- 46 | | 47 | | The default icon to use. This can either be a URL to an image or Slack 48 | | emoji like :ghost: or :heart_eyes:. Set to null to use the default 49 | | set on the Slack webhook 50 | | 51 | */ 52 | 53 | 'icon' => env('SLACK_ICON', ''), 54 | 55 | /* 56 | |------------------------------------------------------------- 57 | | Link names 58 | |------------------------------------------------------------- 59 | | 60 | | Whether names like @regan should be converted into links 61 | | by Slack 62 | | 63 | */ 64 | 65 | 'link_names' => false, 66 | 67 | /* 68 | |------------------------------------------------------------- 69 | | Unfurl links 70 | |------------------------------------------------------------- 71 | | 72 | | Whether Slack should unfurl links to text-based content 73 | | 74 | */ 75 | 76 | 'unfurl_links' => false, 77 | 78 | /* 79 | |------------------------------------------------------------- 80 | | Unfurl media 81 | |------------------------------------------------------------- 82 | | 83 | | Whether Slack should unfurl links to media content such 84 | | as images and YouTube videos 85 | | 86 | */ 87 | 88 | 'unfurl_media' => true, 89 | 90 | /* 91 | |------------------------------------------------------------- 92 | | Markdown in message text 93 | |------------------------------------------------------------- 94 | | 95 | | Whether message text should be interpreted in Slack's Markdown-like 96 | | language. For formatting options, see Slack's help article: http://goo.gl/r4fsdO 97 | | 98 | */ 99 | 100 | 'allow_markdown' => true, 101 | 102 | /* 103 | |------------------------------------------------------------- 104 | | Markdown in attachments 105 | |------------------------------------------------------------- 106 | | 107 | | Which attachment fields should be interpreted in Slack's Markdown-like 108 | | language. By default, Slack assumes that no fields in an attachment 109 | | should be formatted as Markdown. 110 | | 111 | */ 112 | 113 | //'markdown_in_attachments' => [], 114 | 115 | 'markdown_in_attachments' => ['text', 'fields'] 116 | 117 | // Allow Markdown in all fields 118 | // 'markdown_in_attachments' => ['pretext', 'text', 'title', 'fields', 'fallback'] 119 | 120 | ]; 121 | --------------------------------------------------------------------------------