diff --git a/app/Bus/Commands/Subscriber/SubscribeSubscriberCommand.php b/app/Bus/Commands/Subscriber/SubscribeSubscriberCommand.php index 52daa2f42d38..9b021f74ac31 100644 --- a/app/Bus/Commands/Subscriber/SubscribeSubscriberCommand.php +++ b/app/Bus/Commands/Subscriber/SubscribeSubscriberCommand.php @@ -18,6 +18,13 @@ */ final class SubscribeSubscriberCommand { + /** + * The subscriber name (identifier). + * + * @var string + */ + public $name; + /** * The subscriber email. * @@ -25,6 +32,13 @@ final class SubscribeSubscriberCommand */ public $email; + /** + * The url of the Mattermost webhook. + * + * @var string + */ + public $webhook_url; + /** * The subscriber auto verification. * @@ -45,21 +59,26 @@ final class SubscribeSubscriberCommand * @var array */ public $rules = [ - 'email' => 'required|email', + 'email' => 'nullable|email', + 'webhook_url' => 'nullable|url', ]; /** * Create a new subscribe subscriber command instance. * + * @param string $name * @param string $email + * @param string $webhook_url * @param bool $verified * @param array|null $subscriptions * * @return void */ - public function __construct($email, $verified = false, $subscriptions = null) + public function __construct($name, $email, $webhook_url, $verified = false, $subscriptions = null) { + $this->name = $name; $this->email = $email; + $this->webhook_url = $webhook_url; $this->verified = $verified; $this->subscriptions = $subscriptions; } diff --git a/app/Bus/Handlers/Commands/Component/UpdateComponentCommandHandler.php b/app/Bus/Handlers/Commands/Component/UpdateComponentCommandHandler.php index f0ad1e44faab..c51903b60999 100644 --- a/app/Bus/Handlers/Commands/Component/UpdateComponentCommandHandler.php +++ b/app/Bus/Handlers/Commands/Component/UpdateComponentCommandHandler.php @@ -50,7 +50,7 @@ public function handle(UpdateComponentCommand $command) $component = $command->component; $originalStatus = $component->status; - if ($command->status && (int) $originalStatus !== (int) $command->status) { + if ((int) $originalStatus !== (int) $command->status) { // Notify even if the new status is Unknown event(new ComponentStatusWasChangedEvent($this->auth->user(), $component, $originalStatus, $command->status, $command->silent)); } diff --git a/app/Bus/Handlers/Commands/Incident/UpdateIncidentCommandHandler.php b/app/Bus/Handlers/Commands/Incident/UpdateIncidentCommandHandler.php index dff31ccfd2af..d3eae5125b04 100644 --- a/app/Bus/Handlers/Commands/Incident/UpdateIncidentCommandHandler.php +++ b/app/Bus/Handlers/Commands/Incident/UpdateIncidentCommandHandler.php @@ -97,7 +97,7 @@ public function handle(UpdateIncidentCommand $command) // Update the component. if ($component = Component::find($command->component_id)) { execute(new UpdateComponentCommand( - Component::find($command->component_id), + $component, null, null, $command->component_status, diff --git a/app/Bus/Handlers/Commands/IncidentUpdate/CreateIncidentUpdateCommandHandler.php b/app/Bus/Handlers/Commands/IncidentUpdate/CreateIncidentUpdateCommandHandler.php index 20dd2c84a933..13e06598b5a9 100644 --- a/app/Bus/Handlers/Commands/IncidentUpdate/CreateIncidentUpdateCommandHandler.php +++ b/app/Bus/Handlers/Commands/IncidentUpdate/CreateIncidentUpdateCommandHandler.php @@ -69,8 +69,8 @@ public function handle(CreateIncidentUpdateCommand $command) $command->status, null, null, - null, - null, + $command->component_id, + $command->component_status, null, null, null, diff --git a/app/Bus/Handlers/Commands/Subscriber/SubscribeSubscriberCommandHandler.php b/app/Bus/Handlers/Commands/Subscriber/SubscribeSubscriberCommandHandler.php index 1dde43da4a84..d21a381cf793 100644 --- a/app/Bus/Handlers/Commands/Subscriber/SubscribeSubscriberCommandHandler.php +++ b/app/Bus/Handlers/Commands/Subscriber/SubscribeSubscriberCommandHandler.php @@ -33,15 +33,23 @@ class SubscribeSubscriberCommandHandler * * @param \CachetHQ\Cachet\Bus\Commands\Subscriber\SubscribeSubscriberCommand $command * - * @return \CachetHQ\Cachet\Models\Subscriber + * @return boolean (true if created else false) */ public function handle(SubscribeSubscriberCommand $command) { - if ($subscriber = Subscriber::where('email', '=', $command->email)->first()) { - return $subscriber; + if (Subscriber::where('name', '=', $command->name)->first()) { + return false; } - $subscriber = Subscriber::firstOrCreate(['email' => $command->email]); + if ($command->email and Subscriber::where('email', '=', $command->email)->first()) { + return false; + } + + $subscriber = Subscriber::create([ + 'name' => $command->name, + 'email' => $command->email, + 'mattermost_webhook_url' => $command->webhook_url + ]); // Decide what to subscribe the subscriber to. if ($subscriptions = $command->subscriptions) { @@ -67,6 +75,6 @@ public function handle(SubscribeSubscriberCommand $command) $subscriber->load('subscriptions'); - return $subscriber; + return true; } } diff --git a/app/Bus/Handlers/Events/Schedule/SendScheduleEmailNotificationHandler.php b/app/Bus/Handlers/Events/Schedule/SendScheduleEmailNotificationHandler.php index 84e9db31d616..2c0a590af6eb 100644 --- a/app/Bus/Handlers/Events/Schedule/SendScheduleEmailNotificationHandler.php +++ b/app/Bus/Handlers/Events/Schedule/SendScheduleEmailNotificationHandler.php @@ -15,6 +15,8 @@ use CachetHQ\Cachet\Models\Subscriber; use CachetHQ\Cachet\Notifications\Schedule\NewScheduleNotification; +use Illuminate\Support\Facades\Notification; + /** * This is the send schedule event notification handler. * @@ -55,9 +57,8 @@ public function handle(ScheduleEventInterface $event) return false; } - // First notify all global subscribers. - $globalSubscribers = $this->subscriber->isVerified()->isGlobal()->get()->each(function ($subscriber) use ($schedule) { - $subscriber->notify(new NewScheduleNotification($schedule)); - }); + // Notify all global subscribers. + $globalSubscribers = $this->subscriber->isVerified()->isGlobal()->get(); + Notification::send($globalSubscribers, new NewScheduleNotification($schedule)); } } diff --git a/app/Channels/MattermostWebhookChannel.php b/app/Channels/MattermostWebhookChannel.php new file mode 100644 index 000000000000..4a6965713df4 --- /dev/null +++ b/app/Channels/MattermostWebhookChannel.php @@ -0,0 +1,46 @@ +http = $http; + } + + /** + * Send the given notification. + * + * @param mixed $notifiable + * @param \Illuminate\Notifications\Notification $notification + * @return void + */ + public function send($notifiable, Notification $notification) + { + if (! $url = $notifiable->routeNotificationFor('mattermost', $notification)) { + return; + } + + $this->http->post($url, [ + 'json' => $notification->toMattermost($notifiable), + ]); + } +} diff --git a/app/Http/Controllers/Dashboard/IncidentUpdateController.php b/app/Http/Controllers/Dashboard/IncidentUpdateController.php index 092809e374dc..75bba104ff21 100644 --- a/app/Http/Controllers/Dashboard/IncidentUpdateController.php +++ b/app/Http/Controllers/Dashboard/IncidentUpdateController.php @@ -118,12 +118,8 @@ public function createIncidentUpdateAction(Incident $incident) ->withErrors($e->getMessageBag()); } - if ($incident->component) { - $incident->component->update(['status' => Binput::get('component_status')]); - } - return cachet_redirect('dashboard.incidents') - ->withSuccess(sprintf('%s %s', trans('dashboard.notifications.awesome'), trans('dashboard.incidents.updates.success'))); + ->withSuccess(sprintf('%s %s', trans('dashboard.notifications.awesome'), trans('dashboard.incidents.updates.add.success'))); } /** diff --git a/app/Http/Controllers/Dashboard/SubscriberController.php b/app/Http/Controllers/Dashboard/SubscriberController.php index 17de83eb4a2d..f95517542c67 100644 --- a/app/Http/Controllers/Dashboard/SubscriberController.php +++ b/app/Http/Controllers/Dashboard/SubscriberController.php @@ -13,29 +13,69 @@ use AltThree\Validator\ValidationException; use CachetHQ\Cachet\Bus\Commands\Subscriber\SubscribeSubscriberCommand; +use CachetHQ\Cachet\Bus\Commands\Subscriber\SubscribeMattermostHookCommand; use CachetHQ\Cachet\Bus\Commands\Subscriber\UnsubscribeSubscriberCommand; use CachetHQ\Cachet\Models\Subscriber; use GrahamCampbell\Binput\Facades\Binput; use Illuminate\Contracts\Config\Repository; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\View; +use Illuminate\Support\MessageBag; class SubscriberController extends Controller { /** - * Shows the subscribers view. + * Array of sub-menu items. + * + * @var array + */ + protected $subMenu = []; + + /** + * Creates a new subscriber controller instance. + * + * @return void + */ + public function __construct() + { + $this->subMenu = [ + 'email' => [ + 'title' => trans('dashboard.subscribers.channel.email.name'), + 'url' => cachet_route('dashboard.subscribers'), + 'icon' => 'ion ion-ios-email-outline', + 'active' => false, + ], + 'mattermost' => [ + 'title' => trans('dashboard.subscribers.channel.mattermost.name'), + 'url' => cachet_route('dashboard.subscribers.mattermost'), + 'icon' => 'ion ion-paper-airplane', + 'active' => false, + ], + ]; + + View::share([ + 'subMenu' => $this->subMenu, + 'subTitle' => trans('dashboard.subscribers.subscribers'), + ]); + } + + /** + * Shows the subscribers view (for emails). * * @return \Illuminate\View\View */ public function showSubscribers() { + $this->subMenu['email']['active'] = true; + return View::make('dashboard.subscribers.index') - ->withPageTitle(trans('dashboard.subscribers.subscribers').' - '.trans('dashboard.dashboard')) - ->withSubscribers(Subscriber::with('subscriptions.component')->get()); + ->withPageTitle(trans('dashboard.subscribers.channel.email.subscribers').' - '.trans('dashboard.dashboard')) + ->withSubscribers(Subscriber::whereNotNull('email')->with('subscriptions.component')->get()) + ->withSubMenu($this->subMenu); } /** - * Shows the add subscriber view. + * Shows the add subscriber view (for emails). * * @return \Illuminate\View\View */ @@ -46,7 +86,7 @@ public function showAddSubscriber() } /** - * Creates a new subscriber. + * Creates a new (email) subscriber. * * @return \Illuminate\Http\RedirectResponse */ @@ -54,11 +94,16 @@ public function createSubscriberAction() { $verified = app(Repository::class)->get('setting.skip_subscriber_verification'); + $subscriberData = Binput::get('subscriber'); try { - $subscribers = preg_split("/\r\n|\n|\r/", Binput::get('email')); - - foreach ($subscribers as $subscriber) { - execute(new SubscribeSubscriberCommand($subscriber, $verified)); + $created = execute(new SubscribeSubscriberCommand( + $subscriberData['name'], // Name + $subscriberData['email'], // Email + null, // Webhook url + $verified // Verified + )); + if (!$created) { + throw new ValidationException(new MessageBag([trans('dashboard.subscribers.add.email_exists')])); } } catch (ValidationException $e) { return cachet_redirect('dashboard.subscribers.create') @@ -67,7 +112,7 @@ public function createSubscriberAction() ->withErrors($e->getMessageBag()); } - return cachet_redirect('dashboard.subscribers.create') + return cachet_redirect('dashboard.subscribers') ->withSuccess(sprintf('%s %s', trans('dashboard.notifications.awesome'), trans('dashboard.subscribers.add.success'))); } diff --git a/app/Http/Controllers/Dashboard/SubscriberMattermostController.php b/app/Http/Controllers/Dashboard/SubscriberMattermostController.php new file mode 100644 index 000000000000..7d37afd845f2 --- /dev/null +++ b/app/Http/Controllers/Dashboard/SubscriberMattermostController.php @@ -0,0 +1,132 @@ +subMenu = [ + 'email' => [ + 'title' => trans('dashboard.subscribers.channel.email.name'), + 'url' => cachet_route('dashboard.subscribers'), + 'icon' => 'ion ion-ios-email-outline', + 'active' => false, + ], + 'mattermost' => [ + 'title' => trans('dashboard.subscribers.channel.mattermost.name'), + 'url' => cachet_route('dashboard.subscribers.mattermost'), + 'icon' => 'ion ion-paper-airplane', + 'active' => false, + ], + ]; + + View::share([ + 'subMenu' => $this->subMenu, + 'subTitle' => trans('dashboard.subscribers.subscribers'), + ]); + } + + /** + * Shows the subscribers view (for Mattermost channels). + * + * @return \Illuminate\View\View + */ + public function showMattermostSubscribers() + { + $this->subMenu['mattermost']['active'] = true; + + return View::make('dashboard.subscribers.mattermost.index') + ->withPageTitle(trans('dashboard.subscribers.channel.email.subscribers').' - '.trans('dashboard.dashboard')) + ->withSubscribers(Subscriber::whereNotNull('mattermost_webhook_url')->with('subscriptions.component')->get()) + ->withSubMenu($this->subMenu); + } + + /** + * Shows the add subscriber view (for Mattermost channels). + * + * @return \Illuminate\View\View + */ + public function showAddMattermostSubscriber() + { + return View::make('dashboard.subscribers.mattermost.add') + ->withPageTitle(trans('dashboard.subscribers.add.title').' - '.trans('dashboard.dashboard')); + } + + /** + * Creates a new Mattermost subscriber. + * + * @return \Illuminate\Http\RedirectResponse + */ + public function createMattermostSubscriberAction() + { + $subscriberData = Binput::get('subscriber'); + try { + $created = execute(new SubscribeSubscriberCommand( + $subscriberData['name'], // Name + null, // Email + $subscriberData['hook'], // Webhook url + true // Verified + )); + if (!$created) { + throw new ValidationException(new MessageBag([trans('dashboard.subscribers.add.name_exists')])); + } + } catch (ValidationException $e) { + return cachet_redirect('dashboard.subscribers.mattermost.create') + ->withInput(Binput::all()) + ->withTitle(sprintf('%s %s', trans('dashboard.notifications.whoops'), trans('dashboard.subscribers.add.failure'))) + ->withErrors($e->getMessageBag()); + } + + return cachet_redirect('dashboard.subscribers.mattermost') + ->withSuccess(sprintf('%s %s', trans('dashboard.notifications.awesome'), trans('dashboard.subscribers.add.success'))); + } + + /** + * Deletes a Mattermost subscriber. + * + * @param \CachetHQ\Cachet\Models\Subscriber $subscriber + * + * @throws \Exception + * + * @return \Illuminate\Http\RedirectResponse + */ + public function deleteMattermostSubscriberAction(Subscriber $subscriber) + { + execute(new UnsubscribeSubscriberCommand($subscriber)); + + return cachet_redirect('dashboard.subscribers.mattermost'); + } +} diff --git a/app/Http/Routes/Dashboard/SubscriberRoutes.php b/app/Http/Routes/Dashboard/SubscriberRoutes.php index 129d786f8bd8..c7a88b9ebd77 100644 --- a/app/Http/Routes/Dashboard/SubscriberRoutes.php +++ b/app/Http/Routes/Dashboard/SubscriberRoutes.php @@ -47,6 +47,11 @@ public function map(Registrar $router) 'uses' => 'SubscriberController@showSubscribers', ]); + $router->get('mattermost', [ + 'as' => 'get:dashboard.subscribers.mattermost', + 'uses' => 'SubscriberMattermostController@showMattermostSubscribers', + ]); + $router->get('create', [ 'as' => 'get:dashboard.subscribers.create', 'uses' => 'SubscriberController@showAddSubscriber', @@ -56,10 +61,24 @@ public function map(Registrar $router) 'uses' => 'SubscriberController@createSubscriberAction', ]); + $router->get('mattermost/create', [ + 'as' => 'get:dashboard.subscribers.mattermost.create', + 'uses' => 'SubscriberMattermostController@showAddMattermostSubscriber', + ]); + $router->post('mattermost/create', [ + 'as' => 'post:dashboard.subscribers.mattermost.create', + 'uses' => 'SubscriberMattermostController@createMattermostSubscriberAction', + ]); + $router->delete('{subscriber}/delete', [ 'as' => 'delete:dashboard.subscribers.delete', 'uses' => 'SubscriberController@deleteSubscriberAction', ]); + + $router->delete('{subscriber}/mattermost/delete', [ + 'as' => 'delete:dashboard.subscribers.mattermost.delete', + 'uses' => 'SubscriberMattermostController@deleteMattermostSubscriberAction', + ]); }); } } diff --git a/app/Models/Subscriber.php b/app/Models/Subscriber.php index e10a093b16a1..d4f991302747 100644 --- a/app/Models/Subscriber.php +++ b/app/Models/Subscriber.php @@ -39,12 +39,14 @@ class Subscriber extends Model implements HasPresenter * @var string[] */ protected $casts = [ - 'email' => 'string', - 'phone_number' => 'string', - 'slack_webhook_url' => 'string', - 'verify_code' => 'string', - 'verified_at' => 'date', - 'global' => 'bool', + 'name' => 'string', + 'email' => 'string', + 'phone_number' => 'string', + 'slack_webhook_url' => 'string', + 'mattermost_webhook_url' => 'string', + 'verify_code' => 'string', + 'verified_at' => 'date', + 'global' => 'bool', ]; /** @@ -53,9 +55,11 @@ class Subscriber extends Model implements HasPresenter * @var string[] */ protected $fillable = [ + 'name', 'email', 'phone_number', 'slack_webhook_url', + 'mattermost_webhook_url', 'verified_at', 'global', ]; @@ -66,9 +70,11 @@ class Subscriber extends Model implements HasPresenter * @var string[] */ public $rules = [ - 'email' => 'nullable|email', - 'phone_number' => 'nullable|string', - 'slack_webhook_url' => 'nullable|url', + 'name' => 'string', + 'email' => 'nullable|email', + 'phone_number' => 'nullable|string', + 'slack_webhook_url' => 'nullable|url', + 'mattermost_webhook_url' => 'nullable|url', ]; /** @@ -128,6 +134,18 @@ public function scopeIsGlobal(Builder $query) return $query->where('global', '=', true); } + /** + * Scope Mattermost subscribers. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIsMattermost(Builder $query) + { + return $query->whereNotNull('mattermost_webhook_url'); + } + /** * Finds all verified subscriptions for a component. * @@ -183,6 +201,16 @@ public function routeNotificationForSlack() return $this->slack_webhook_url; } + /** + * Route notifications for the Mattermost channel. + * + * @return string + */ + public function routeNotificationForMattermost() + { + return $this->mattermost_webhook_url; + } + /** * Get the presenter class. * diff --git a/app/Notifications/Component/ComponentStatusChangedNotification.php b/app/Notifications/Component/ComponentStatusChangedNotification.php index 0500dab01177..f1b384b634c4 100644 --- a/app/Notifications/Component/ComponentStatusChangedNotification.php +++ b/app/Notifications/Component/ComponentStatusChangedNotification.php @@ -17,6 +17,7 @@ use Illuminate\Notifications\Messages\NexmoMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\URL; use McCool\LaravelAutoPresenter\Facades\AutoPresenter; @@ -66,7 +67,7 @@ public function __construct(Component $component, $status) */ public function via($notifiable) { - return ['mail', 'nexmo', 'slack']; + return ['mail', 'nexmo', 'slack', 'mattermost']; } /** @@ -155,4 +156,47 @@ public function toSlack($notifiable) ->footer(trans('cachet.subscriber.unsubscribe', ['link' => cachet_route('subscribe.unsubscribe', $notifiable->verify_code)])); }); } + + /** + * Get the Mattermost representation of the notification. + * + * @param mixed $notifiable + * + * @return an array meant to be converted to a json payload + */ + public function toMattermost($notifiable) + { + $fallback_content = trans('notifications.component.status_update.mattermost.content', [ + 'name' => $this->component->name, + 'old_status' => $this->component->human_status, + 'new_status' => trans("cachet.components.status.{$this->status}"), + ]); + + switch ($this->status) { + case 0: $status_color = '#BABABA'; break; // Grey (Unknown) + case 1: $status_color = '#00D11C'; break; // Green (Operational) + case 2: $status_color = '#FFC524'; break; // Yellow (Performance Issues) + case 3: $status_color = '#FF8000'; break; // Orange (Partial Outage) + case 4: $status_color = '#FF4400'; break; // Red (Major Outage) + } + + $final_data = [ + 'text' => trans('notifications.component.status_update.mattermost.title'), + 'attachments' => [ + array_filter([ + 'fallback' => $fallback_content, + 'title' => $this->component->name, + 'text' => trans('notifications.component.status_update.mattermost.content_short', [ + 'old_status' => $this->component->human_status, + 'new_status' => trans("cachet.components.status.{$this->status}"), + ]), + 'author_name' => Config::get('setting.app_name'), + 'author_icon' => asset('img/cachet-icon.png'), + 'color' => $status_color, + ]) + ], + ]; + + return $final_data; + } } diff --git a/app/Notifications/Incident/NewIncidentNotification.php b/app/Notifications/Incident/NewIncidentNotification.php index 6850e3056bfa..24a35460e79e 100644 --- a/app/Notifications/Incident/NewIncidentNotification.php +++ b/app/Notifications/Incident/NewIncidentNotification.php @@ -58,7 +58,7 @@ public function __construct(Incident $incident) */ public function via($notifiable) { - return ['mail', 'nexmo', 'slack']; + return ['mail', 'nexmo', 'slack', 'mattermost']; } /** @@ -139,4 +139,62 @@ public function toSlack($notifiable) ])); }); } + + /** + * Get the Mattermost representation of the notification. + * + * @param mixed $notifiable + * + * @return an array meant to be converted to a json payload + */ + public function toMattermost($notifiable) + { + if ($this->incident->status === Incident::FIXED) { + $status_color = '#00D11C'; // Green + } elseif ($this->incident->status === Incident::WATCHED) { + $status_color = '#FF8000'; // Orange + } else { + $status_color = '#FF4400'; // Red + } + + $component_name = ''; + $component_status = ''; + if ($this->incident->component) { + $component_name = $this->incident->component->name; + $component_status = trans("cachet.components.status.{$this->incident->component->status}"); + } + + $final_data = [ + 'text' => trans('notifications.incident.new.mattermost.title'), + 'attachments' => [ + array_filter([ + 'fallback' => trans('notifications.incident.new.mattermost.content', [ + 'name' => $this->incident->name, + 'app_name' => Config::get('setting.app_name') + ]), + 'color' => $status_color, + 'title' => $this->incident->name, + 'text' => $this->incident->message."\n[".trans('notifications.incident.new.mattermost.action')."](".cachet_route('incident', [$this->incident]).")", + 'author_name' => Config::get('setting.app_name'), + 'author_icon' => asset('img/cachet-icon.png'), + 'footer' => "#{$this->incident->id}", + 'fields' => array_filter([ + [ + 'short' => true, + 'title' => trans('notifications.incident.new.mattermost.component'), + 'value' => $component_name, + ], + [ + 'short' => true, + 'title' => trans('notifications.incident.new.mattermost.status'), + 'value' => $component_status, + ], + ], + function($array) { return !empty($array['value']); }), + ]) + ], + ]; + + return $final_data; + } } diff --git a/app/Notifications/IncidentUpdate/IncidentUpdatedNotification.php b/app/Notifications/IncidentUpdate/IncidentUpdatedNotification.php index 11e113958c6d..7061395da15d 100644 --- a/app/Notifications/IncidentUpdate/IncidentUpdatedNotification.php +++ b/app/Notifications/IncidentUpdate/IncidentUpdatedNotification.php @@ -18,6 +18,7 @@ use Illuminate\Notifications\Messages\NexmoMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\URL; use McCool\LaravelAutoPresenter\Facades\AutoPresenter; @@ -58,7 +59,7 @@ public function __construct(IncidentUpdate $update) */ public function via($notifiable) { - return ['mail', 'nexmo', 'slack']; + return ['mail', 'nexmo', 'slack', 'mattermost']; } /** @@ -150,4 +151,60 @@ public function toSlack($notifiable) ->footer(trans('cachet.subscriber.unsubscribe', ['link' => cachet_route('subscribe.unsubscribe', $notifiable->verify_code)])); }); } + + /** + * Get the Mattermost representation of the notification. + * + * @param mixed $notifiable + * + * @return an array meant to be converted to a json payload + */ + public function toMattermost($notifiable) + { + if ($this->update->status === Incident::FIXED) { + $status_color = '#00D11C'; // Green + } elseif ($this->update->status === Incident::WATCHED) { + $status_color = '#FF8000'; // Orange + } else { + $status_color = '#FF4400'; // Red + } + + $component_name = ''; + $component_status = ''; + if ($this->update->incident->component) { + $component_name = $this->update->incident->component->name; + $component_status = trans("cachet.components.status.{$this->update->incident->component->status}"); + } + + + $final_data = [ + 'text' => trans('notifications.incident.update.mattermost.title'), + 'attachments' => [ + array_filter([ + 'fallback' => trans('notifications.incident.update.mattermost.content', ['name' => $this->update->incident->name, 'new_status' => $this->update->human_status]), + 'color' => $status_color, + 'title' => '['.$this->update->human_status.'] '.$this->update->incident->name, + 'text' => $this->update->message."\n[".trans('notifications.incident.update.mattermost.action')."](".cachet_route('incident', [$this->update->incident]).")", + 'author_name' => Config::get('setting.app_name'), + 'author_icon' => asset('img/cachet-icon.png'), + 'footer' => "#{$this->update->id}", + 'fields' => array_filter([ + [ + 'short' => true, + 'title' => trans('notifications.incident.update.mattermost.component'), + 'value' => $component_name, + ], + [ + 'short' => true, + 'title' => trans('notifications.incident.update.mattermost.status'), + 'value' => $component_status, + ], + ], + function($array) { return !empty($array['value']); }), + ]) + ], + ]; + + return $final_data; + } } diff --git a/app/Notifications/Schedule/NewScheduleNotification.php b/app/Notifications/Schedule/NewScheduleNotification.php index d89d72cdd23c..acca5158d7cf 100644 --- a/app/Notifications/Schedule/NewScheduleNotification.php +++ b/app/Notifications/Schedule/NewScheduleNotification.php @@ -13,11 +13,11 @@ use CachetHQ\Cachet\Models\Schedule; use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\NexmoMessage; use Illuminate\Notifications\Messages\SlackMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\URL; use McCool\LaravelAutoPresenter\Facades\AutoPresenter; @@ -26,7 +26,7 @@ * * @author James Brooks */ -class NewScheduleNotification extends Notification implements ShouldQueue +class NewScheduleNotification extends Notification { use Queueable; @@ -58,7 +58,7 @@ public function __construct(Schedule $schedule) */ public function via($notifiable) { - return ['mail', 'nexmo', 'slack']; + return ['mail', 'nexmo', 'slack', 'mattermost']; } /** @@ -130,4 +130,53 @@ public function toSlack($notifiable) ])); }); } + + /** + * Get the Mattermost representation of the notification. + * + * @param mixed $notifiable + * + * @return an array meant to be converted to a json payload + */ + public function toMattermost($notifiable) + { + $fallback_content = trans('notifications.schedule.new.mattermost.content', [ + 'name' => $this->schedule->name, + 'date' => $this->schedule->scheduled_at_formatted, + ]); + + $final_data = [ + 'text' => trans('notifications.schedule.new.mattermost.title'), + 'attachments' => [ + array_filter([ + 'fallback' => $fallback_content, + 'title' => $this->schedule->name, + 'text' => $this->schedule->message, + 'author_name' => Config::get('setting.app_name'), + 'author_icon' => asset('img/cachet-icon.png'), + 'footer' => "#{$this->schedule->id}", + 'fields' => array_filter([ + [ + 'short' => false, + 'title' => trans('notifications.schedule.new.mattermost.status'), + 'value' => $this->schedule->human_status, + ], + [ + 'short' => true, + 'title' => trans('notifications.schedule.new.mattermost.start'), + 'value' => $this->schedule->scheduled_at_formatted, + ], + [ + 'short' => true, + 'title' => trans('notifications.schedule.new.mattermost.end'), + 'value' => $this->schedule->completed_at_formatted, + ], + ], + function($array) { return !empty($array['value']); }), + ]) + ], + ]; + + return $final_data; + } } diff --git a/app/Providers/MattermostChannelServiceProvider.php b/app/Providers/MattermostChannelServiceProvider.php new file mode 100644 index 000000000000..57c734a2d20d --- /dev/null +++ b/app/Providers/MattermostChannelServiceProvider.php @@ -0,0 +1,24 @@ +string('name')->default('')->first(); + $table->string('mattermost_webhook_url')->nullable()->default(null)->after('slack_webhook_url'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('subscribers', function (Blueprint $table) { + $table->dropColumn(['name', 'mattermost_webhook_url']); + }); + } +} diff --git a/resources/lang/en/dashboard.php b/resources/lang/en/dashboard.php index a72689a5a623..48c4e0c88ceb 100644 --- a/resources/lang/en/dashboard.php +++ b/resources/lang/en/dashboard.php @@ -161,14 +161,27 @@ 'description_disabled' => 'To use this feature, you need allow people to signup for notifications.', 'verified' => 'Verified', 'not_verified' => 'Not verified', - 'subscriber' => ':email, subscribed :date', + 'subscribed_at' => 'subscribed :date', 'no_subscriptions' => 'Subscribed to all updates', 'global' => 'Globally subscribed', + 'channel' => [ + 'email' => [ + 'name' => 'Email', + 'subscribers' => 'Email subscribers', + 'description' => 'Email subscribers will receive email updates when incidents are created or components are updated.', + ], + 'mattermost' => [ + 'name' => 'Mattermost', + 'subscribers' => 'Mattermost subscribers', + 'description' => 'Mattermost subscribers are bots that will post updates on a Mattermost channel when incidents are created or components are updated.', + ], + ], 'add' => [ - 'title' => 'Add a new subscriber', - 'success' => 'Subscriber has been added!', - 'failure' => 'Something went wrong adding the subscriber, please try again.', - 'help' => 'Enter each subscriber on a new line.', + 'title' => 'Add a new subscriber', + 'success' => 'Subscriber has been added!', + 'failure' => 'Something went wrong adding the subscriber, please try again.', + 'name_exists' => 'A subscriber with the same name already exists.', + 'email_exists' => 'A subscriber with the same email or name already exists.', ], 'edit' => [ 'title' => 'Update subscriber', diff --git a/resources/lang/en/forms.php b/resources/lang/en/forms.php index 5c5a41a9c5d5..1926699b588f 100644 --- a/resources/lang/en/forms.php +++ b/resources/lang/en/forms.php @@ -225,6 +225,20 @@ ], ], + 'channel' => [ + 'mattermost' => [ + 'hook' => 'Mattermost webhook', + 'hook-help' => 'URL', + 'name' => 'Name', + 'name-help' => 'Identifier of the subscriber (e.g. channel name)', + ], + 'email' => [ + 'email' => 'Email', + 'name' => 'Name', + 'name-help' => 'Identifier of the subscriber (e.g. username or real name)', + ], + ], + 'general' => [ 'timezone' => 'Select Timezone', ], diff --git a/resources/lang/en/notifications.php b/resources/lang/en/notifications.php index efd9e0e58b3a..e299f7114367 100644 --- a/resources/lang/en/notifications.php +++ b/resources/lang/en/notifications.php @@ -23,6 +23,12 @@ 'content' => ':name status changed from :old_status to :new_status.', 'action' => 'View', ], + 'mattermost' => [ + 'title' => 'Component Status Updated', + 'action' => 'View', + 'content' => ':name status changed from :old_status to :new_status.', + 'content_short' => 'Status changed from **:old_status** to **:new_status**.', + ], 'slack' => [ 'title' => 'Component Status Updated', 'content' => ':name status changed from :old_status to :new_status.', @@ -40,6 +46,13 @@ 'content' => 'Incident :name was reported', 'action' => 'View', ], + 'mattermost' => [ + 'title' => 'New Incident Reported', + 'action' => 'View', + 'content' => 'A new incident, :name, was reported at :app_name', + 'component' => 'Component', + 'status' => 'Current Status', + ], 'slack' => [ 'title' => 'Incident :name Reported', 'content' => 'A new incident was reported at :app_name', @@ -55,6 +68,13 @@ 'title' => ':name was updated to :new_status', 'action' => 'View', ], + 'mattermost' => [ + 'title' => 'Incident Updated', + 'action' => 'View', + 'content' => ':name was updated to :new_status', + 'component' => 'Component', + 'status' => 'Current Status', + ], 'slack' => [ 'title' => ':name Updated', 'content' => ':name was updated to :new_status', @@ -72,6 +92,14 @@ 'title' => 'A new scheduled maintenance was created.', 'action' => 'View', ], + 'mattermost' => [ + 'title' => 'New Scheduled Maintenance Created', + 'action' => 'View', + 'content' => ':name was scheduled for :date', + 'status' => 'Status', + 'start' => 'Scheduled at', + 'end' => 'Planned completion at', + ], 'slack' => [ 'title' => 'New Schedule Created!', 'content' => ':name was scheduled for :date', diff --git a/resources/views/dashboard/subscribers/add.blade.php b/resources/views/dashboard/subscribers/add.blade.php index dd15f7c252ea..097c4bffdf69 100644 --- a/resources/views/dashboard/subscribers/add.blade.php +++ b/resources/views/dashboard/subscribers/add.blade.php @@ -6,7 +6,7 @@ - {{ trans('dashboard.subscribers.subscribers') }} + {{ trans('dashboard.subscribers.channel.email.subscribers') }}
@@ -17,9 +17,12 @@
- - - {{ trans('dashboard.subscribers.add.help') }} + + +
+
+ +
diff --git a/resources/views/dashboard/subscribers/index.blade.php b/resources/views/dashboard/subscribers/index.blade.php index 2ac47e704b83..af0ff595f419 100644 --- a/resources/views/dashboard/subscribers/index.blade.php +++ b/resources/views/dashboard/subscribers/index.blade.php @@ -1,57 +1,63 @@ @extends('layout.dashboard') @section('content') -
- - - {{ trans('dashboard.subscribers.subscribers') }} - - @if($currentUser->isAdmin) - - {{ trans('dashboard.subscribers.add.title') }} - - @endif -
-
-
-
-
-

- {{ trans('dashboard.subscribers.description') }} -

+
+ @includeWhen(isset($subMenu), 'dashboard.partials.sub-sidebar') +
+
+ + {{ trans('dashboard.subscribers.channel.email.subscribers') }} + + @if($currentUser->isAdmin) + + {{ trans('dashboard.subscribers.add.title') }} + + @endif +
+
+
+
+

+ {{ trans('dashboard.subscribers.channel.email.description') }} +

-
- @foreach($subscribers as $subscriber) -
-
-

{{ trans('dashboard.subscribers.subscriber', ['email' => $subscriber->email, 'date' => $subscriber->created_at]) }}

-
-
- @if(is_null($subscriber->getOriginal('verified_at'))) - {{ trans('dashboard.subscribers.not_verified') }} - @else - {{ trans('dashboard.subscribers.verified') }} - @endif -
-
- @if($subscriber->global) -

{{ trans('dashboard.subscribers.global') }}

- @elseif($subscriber->subscriptions->isNotEmpty()) - {!! $subscriber->subscriptions->map(function ($subscription) { - return sprintf('%s', $subscription->component->name); - })->implode(' ') !!} - @else -

{{ trans('dashboard.subscribers.no_subscriptions') }}

- @endif -
-
- {{ trans('forms.edit') }} - {{ trans('forms.delete') }} +
+ @foreach($subscribers as $subscriber) +
+
+

{{ $subscriber->name }}

+
+
+

{{ $subscriber->email }}

+
+
+

{{ trans('dashboard.subscribers.subscribed_at', ['date' => $subscriber->created_at]) }}

+
+
+ @if(is_null($subscriber->getOriginal('verified_at'))) + {{ trans('dashboard.subscribers.not_verified') }} + @else + {{ trans('dashboard.subscribers.verified') }} + @endif +
+
+ @if($subscriber->global) +

{{ trans('dashboard.subscribers.global') }}

+ @elseif($subscriber->subscriptions->isNotEmpty()) + {!! $subscriber->subscriptions->map(function ($subscription) { + return sprintf('%s', $subscription->component->name); + })->implode(' ') !!} + @else +

{{ trans('dashboard.subscribers.no_subscriptions') }}

+ @endif +
+
+ @endforeach
- @endforeach
diff --git a/resources/views/dashboard/subscribers/mattermost/add.blade.php b/resources/views/dashboard/subscribers/mattermost/add.blade.php new file mode 100644 index 000000000000..44b132e9c1aa --- /dev/null +++ b/resources/views/dashboard/subscribers/mattermost/add.blade.php @@ -0,0 +1,39 @@ +@extends('layout.dashboard') + +@section('content') +
+ + + {{ trans('dashboard.subscribers.channel.mattermost.subscribers') }} + +
+
+
+
+ @include('partials.errors') +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + {{ trans('forms.cancel') }} +
+
+
+
+
+
+@stop diff --git a/resources/views/dashboard/subscribers/mattermost/index.blade.php b/resources/views/dashboard/subscribers/mattermost/index.blade.php new file mode 100644 index 000000000000..ae9e0f3805b8 --- /dev/null +++ b/resources/views/dashboard/subscribers/mattermost/index.blade.php @@ -0,0 +1,55 @@ +@extends('layout.dashboard') + +@section('content') +
+ @includeWhen(isset($subMenu), 'dashboard.partials.sub-sidebar') +
+
+ + {{ trans('dashboard.subscribers.channel.mattermost.subscribers') }} + + @if($currentUser->isAdmin) + + {{ trans('dashboard.subscribers.add.title') }} + + @endif +
+
+
+
+

+ {{ trans('dashboard.subscribers.channel.mattermost.description') }} +

+ +
+ @foreach($subscribers as $subscriber) +
+
+

{{ $subscriber->name }}

+
+
+

{{ trans('dashboard.subscribers.subscribed_at', ['date' => $subscriber->created_at]) }}

+
+
+ @if($subscriber->global) +

{{ trans('dashboard.subscribers.global') }}

+ @elseif($subscriber->subscriptions->isNotEmpty()) + {!! $subscriber->subscriptions->map(function ($subscription) { + return sprintf('%s', $subscription->component->name); + })->implode(' ') !!} + @else +

{{ trans('dashboard.subscribers.no_subscriptions') }}

+ @endif +
+ +
+ @endforeach +
+
+
+
+
+@stop