diff --git a/app/Actions/Notifications/SendPushoverTestNotification.php b/app/Actions/Notifications/SendPushoverTestNotification.php new file mode 100644 index 000000000..d37b0594d --- /dev/null +++ b/app/Actions/Notifications/SendPushoverTestNotification.php @@ -0,0 +1,41 @@ +title('You need to add Pushover URLs!') + ->warning() + ->send(); + + return; + } + + foreach ($webhooks as $webhook) { + WebhookCall::create() + ->url($webhook['url']) + ->payload([ + 'token' => $webhook['api_token'], + 'user' => $webhook['user_key'], + 'message' => '👋 Testing the Pushover notification channel.', + ]) + ->doNotSign() + ->dispatch(); + } + + Notification::make() + ->title('Test Pushover notification sent.') + ->success() + ->send(); + } +} diff --git a/app/Filament/Pages/Settings/NotificationPage.php b/app/Filament/Pages/Settings/NotificationPage.php index 652ec4a9f..f3faa9687 100755 --- a/app/Filament/Pages/Settings/NotificationPage.php +++ b/app/Filament/Pages/Settings/NotificationPage.php @@ -7,6 +7,7 @@ use App\Actions\Notifications\SendGotifyTestNotification; use App\Actions\Notifications\SendHealthCheckTestNotification; use App\Actions\Notifications\SendMailTestNotification; +use App\Actions\Notifications\SendPushoverTestNotification; use App\Actions\Notifications\SendSlackTestNotification; use App\Actions\Notifications\SendTelegramTestNotification; use App\Actions\Notifications\SendWebhookTestNotification; @@ -87,6 +88,63 @@ public function form(Form $form): Form 'md' => 2, ]), + Forms\Components\Section::make('Pushover') + ->schema([ + Forms\Components\Toggle::make('pushover_enabled') + ->label('Enable Pushover webhook notifications') + ->reactive() + ->columnSpanFull(), + Forms\Components\Grid::make([ + 'default' => 1, + ]) + ->hidden(fn (Forms\Get $get) => $get('pushover_enabled') !== true) + ->schema([ + Forms\Components\Fieldset::make('Triggers') + ->schema([ + Forms\Components\Toggle::make('pushover_on_speedtest_run') + ->label('Notify on every speedtest run') + ->columnSpanFull(), + Forms\Components\Toggle::make('pushover_on_threshold_failure') + ->label('Notify on threshold failures') + ->columnSpanFull(), + ]), + Forms\Components\Repeater::make('pushover_webhooks') + ->label('Pushover Webhooks') + ->schema([ + Forms\Components\TextInput::make('url') + ->label('URL') + ->placeholder('http://api.pushover.net/1/messages.json') + ->maxLength(2000) + ->required() + ->url(), + Forms\Components\TextInput::make('user_key') + ->label('User Key') + ->placeholder('Your Pushover User Key') + ->maxLength(200) + ->required(), + Forms\Components\TextInput::make('api_token') + ->label('API Token') + ->placeholder('Your Pushover API Token') + ->maxLength(200) + ->required(), + ]) + ->columnSpanFull(), + Forms\Components\Actions::make([ + Forms\Components\Actions\Action::make('test pushover') + ->label('Test Pushover webhook') + ->action(fn (Forms\Get $get) => SendPushoverTestNotification::run( + webhooks: $get('pushover_webhooks') + )) + ->hidden(fn (Forms\Get $get) => ! count($get('pushover_webhooks'))), + ]), + ]), + ]) + ->compact() + ->columns([ + 'default' => 1, + 'md' => 2, + ]), + Forms\Components\Section::make('Discord') ->schema([ Forms\Components\Toggle::make('discord_enabled') diff --git a/app/Listeners/Pushover/SendSpeedtestCompletedNotification.php b/app/Listeners/Pushover/SendSpeedtestCompletedNotification.php new file mode 100644 index 000000000..606fdf8d8 --- /dev/null +++ b/app/Listeners/Pushover/SendSpeedtestCompletedNotification.php @@ -0,0 +1,62 @@ +pushover_enabled) { + return; + } + + if (! $notificationSettings->pushover_on_speedtest_run) { + return; + } + + if (! count($notificationSettings->pushover_webhooks)) { + Log::warning('Pushover urls not found, check Pushover notification channel settings.'); + + return; + } + + $payload = [ + view('pushover.speedtest-completed', [ + 'id' => $event->result->id, + 'service' => Str::title($event->result->service), + 'serverName' => $event->result->server_name, + 'serverId' => $event->result->server_id, + 'isp' => $event->result->isp, + 'ping' => round($event->result->ping).' ms', + 'download' => Number::toBitRate(bits: $event->result->download_bits, precision: 2), + 'upload' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2), + 'packetLoss' => $event->result->packet_loss, + 'url' => url('/admin/results'), + ])->render(), + ]; + + foreach ($notificationSettings->pushover_webhooks as $url) { + WebhookCall::create() + ->url($url['url']) + ->payload([ + 'token' => $url['api_token'], + 'user' => $url['user_key'], + 'message' => $payload, + ]) + ->doNotSign() + ->dispatch(); + } + } +} diff --git a/app/Listeners/Pushover/SendSpeedtestThresholdNotification.php b/app/Listeners/Pushover/SendSpeedtestThresholdNotification.php new file mode 100644 index 000000000..a13179d08 --- /dev/null +++ b/app/Listeners/Pushover/SendSpeedtestThresholdNotification.php @@ -0,0 +1,136 @@ +pushover_enabled) { + return; + } + + if (! $notificationSettings->pushover_on_threshold_failure) { + return; + } + + if (! count($notificationSettings->pushover_webhooks)) { + Log::warning('Pushover urls not found, check Pushover notification channel settings.'); + + return; + } + + $thresholdSettings = new ThresholdSettings(); + + if (! $thresholdSettings->absolute_enabled) { + return; + } + + $failed = []; + + if ($thresholdSettings->absolute_download > 0) { + array_push($failed, $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings)); + } + + if ($thresholdSettings->absolute_upload > 0) { + array_push($failed, $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings)); + } + + if ($thresholdSettings->absolute_ping > 0) { + array_push($failed, $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings)); + } + + $failed = array_filter($failed); + + if (! count($failed)) { + Log::warning('Failed Pushover thresholds not found, won\'t send notification.'); + + return; + } + + $payload = [ + view('pushover.speedtest-threshold', [ + 'id' => $event->result->id, + 'service' => Str::title($event->result->service), + 'serverName' => $event->result->server_name, + 'serverId' => $event->result->server_id, + 'isp' => $event->result->isp, + 'metrics' => $failed, + 'url' => url('/admin/results'), + ])->render(), + ]; + + foreach ($notificationSettings->pushover_webhooks as $url) { + WebhookCall::create() + ->url($url['url']) + ->payload([ + 'token' => $url['api_token'], + 'user' => $url['user_key'], + 'message' => $payload, + ]) + ->doNotSign() + ->dispatch(); + } + } + + /** + * Build Pushover notification if absolute download threshold is breached. + */ + protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array + { + if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) { + return false; + } + + return [ + 'name' => 'Download', + 'threshold' => $thresholdSettings->absolute_download.' Mbps', + 'value' => Number::toBitRate(bits: $event->result->download_bits, precision: 2), + ]; + } + + /** + * Build Pushover notification if absolute upload threshold is breached. + */ + protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array + { + if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) { + return false; + } + + return [ + 'name' => 'Upload', + 'threshold' => $thresholdSettings->absolute_upload.' Mbps', + 'value' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2), + ]; + } + + /** + * Build Pushover notification if absolute ping threshold is breached. + */ + protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array + { + if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) { + return false; + } + + return [ + 'name' => 'Ping', + 'threshold' => $thresholdSettings->absolute_ping.' ms', + 'value' => round($event->result->ping, 2).' ms', + ]; + } +} diff --git a/app/Settings/NotificationSettings.php b/app/Settings/NotificationSettings.php index bf88f72a4..f41f962e6 100644 --- a/app/Settings/NotificationSettings.php +++ b/app/Settings/NotificationSettings.php @@ -46,6 +46,14 @@ class NotificationSettings extends Settings public ?array $discord_webhooks; + public bool $pushover_enabled; + + public bool $pushover_on_speedtest_run; + + public bool $pushover_on_threshold_failure; + + public ?array $pushover_webhooks; + public bool $healthcheck_enabled; public bool $healthcheck_on_speedtest_run; diff --git a/database/settings/2024_02_22_144680_create_pushover_notification_settings.php b/database/settings/2024_02_22_144680_create_pushover_notification_settings.php new file mode 100644 index 000000000..92f394001 --- /dev/null +++ b/database/settings/2024_02_22_144680_create_pushover_notification_settings.php @@ -0,0 +1,14 @@ +migrator->add('notification.pushover_enabled', false); + $this->migrator->add('notification.pushover_on_speedtest_run', false); + $this->migrator->add('notification.pushover_on_threshold_failure', false); + $this->migrator->add('notification.pushover_webhooks', null); + } +}; diff --git a/resources/views/pushover/speedtest-completed.blade.php b/resources/views/pushover/speedtest-completed.blade.php new file mode 100644 index 000000000..2bd6a8581 --- /dev/null +++ b/resources/views/pushover/speedtest-completed.blade.php @@ -0,0 +1,12 @@ +Speedtest Completed - #{{ $id }} + +A new speedtest was completed using {{ $service }}. + +- Server name: {{ $serverName }} +- Server ID: {{ $serverId }} +- ISP: {{ $isp }} +- Ping: {{ $ping }} +- Download: {{ $download }} +- Upload: {{ $upload }} +- Packet Loss: {{ $packetLoss }} % +- URL: {{ $url }} diff --git a/resources/views/pushover/speedtest-threshold.blade.php b/resources/views/pushover/speedtest-threshold.blade.php new file mode 100644 index 000000000..3bc1830ab --- /dev/null +++ b/resources/views/pushover/speedtest-threshold.blade.php @@ -0,0 +1,8 @@ +Speedtest Threshold Breached - #{{ $id }} + +A new speedtest was completed using {{ $service }} on {{ $isp }} but a threshold was breached. + +@foreach ($metrics as $item) +- {{ $item['name'] }} {{ $item['threshold'] }}: {{ $item['value'] }} +@endforeach +- URL: {{ $url }}