diff --git a/app/Actions/Notifications/SendDingTalkTestNotification.php b/app/Actions/Notifications/SendDingTalkTestNotification.php new file mode 100644 index 000000000..505bfd1f3 --- /dev/null +++ b/app/Actions/Notifications/SendDingTalkTestNotification.php @@ -0,0 +1,44 @@ +title('You need to add dingtalk urls!') + ->warning() + ->send(); + + return; + } + + $payload = [ + 'msgtype' => 'text', + 'text' => [ + 'content' => '👋 Testing the DingTalk notification channel on Speedtest Tracker.', + ], + ]; + + foreach ($webhooks as $webhook) { + WebhookCall::create() + ->url($webhook['url']) + ->payload($payload) + ->doNotSign() + ->dispatch(); + } + + Notification::make() + ->title('Test dingtalk notification sent.') + ->success() + ->send(); + } +} diff --git a/app/Actions/Notifications/SendWeComTestNotification.php b/app/Actions/Notifications/SendWeComTestNotification.php new file mode 100644 index 000000000..ded6942bd --- /dev/null +++ b/app/Actions/Notifications/SendWeComTestNotification.php @@ -0,0 +1,44 @@ +title('You need to add wecom urls!') + ->warning() + ->send(); + + return; + } + + $payload = [ + 'msgtype' => 'text', + 'text' => [ + 'content' => '👋 Testing the WeCom notification channel on Speedtest Tracker.', + ], + ]; + + foreach ($webhooks as $webhook) { + WebhookCall::create() + ->url($webhook['url']) + ->payload($payload) + ->doNotSign() + ->dispatch(); + } + + Notification::make() + ->title('Test wecom notification sent.') + ->success() + ->send(); + } +} diff --git a/app/Filament/Pages/Settings/NotificationPage.php b/app/Filament/Pages/Settings/NotificationPage.php index bd7df5902..28f53f8be 100755 --- a/app/Filament/Pages/Settings/NotificationPage.php +++ b/app/Filament/Pages/Settings/NotificationPage.php @@ -3,6 +3,7 @@ namespace App\Filament\Pages\Settings; use App\Actions\Notifications\SendDatabaseTestNotification; +use App\Actions\Notifications\SendDingTalkTestNotification; use App\Actions\Notifications\SendDiscordTestNotification; use App\Actions\Notifications\SendGotifyTestNotification; use App\Actions\Notifications\SendHealthCheckTestNotification; @@ -12,6 +13,7 @@ use App\Actions\Notifications\SendSlackTestNotification; use App\Actions\Notifications\SendTelegramTestNotification; use App\Actions\Notifications\SendWebhookTestNotification; +use App\Actions\Notifications\SendWeComTestNotification; use App\Settings\NotificationSettings; use Filament\Forms; use Filament\Forms\Form; @@ -517,6 +519,95 @@ public function form(Form $form): Form 'default' => 1, 'md' => 2, ]), + + Forms\Components\Section::make('DingTalk') + ->schema([ + Forms\Components\Toggle::make('dingtalk_enabled') + ->label('Enable dingtalk notifications') + ->reactive() + ->columnSpanFull(), + Forms\Components\Grid::make([ + 'default' => 1, + ]) + ->hidden(fn (Forms\Get $get) => $get('dingtalk_enabled') !== true) + ->schema([ + Forms\Components\Fieldset::make('Triggers') + ->schema([ + Forms\Components\Toggle::make('dingtalk_on_speedtest_run') + ->label('Notify on every speedtest run') + ->columnSpan(2), + Forms\Components\Toggle::make('dingtalk_on_threshold_failure') + ->label('Notify on threshold failures') + ->columnSpan(2), + ]), + Forms\Components\Repeater::make('dingtalk_webhooks') + ->label('Webhooks') + ->schema([ + Forms\Components\TextInput::make('url') + ->placeholder('https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxx') + ->maxLength(2000) + ->required() + ->url(), + ]) + ->columnSpanFull(), + Forms\Components\Actions::make([ + Forms\Components\Actions\Action::make('test dingtalk') + ->label('Test dingtalk channel') + ->action(fn (Forms\Get $get) => SendDingTalkTestNotification::run(webhooks: $get('dingtalk_webhooks'))) + ->hidden(fn (Forms\Get $get) => ! count($get('dingtalk_webhooks'))), + ]), + ]), + ]) + ->compact() + ->columns([ + 'default' => 1, + 'md' => 2, + ]), + + Forms\Components\Section::make('WeCom') + ->schema([ + Forms\Components\Toggle::make('wecom_enabled') + ->label('Enable wecom notifications') + ->reactive() + ->columnSpanFull(), + Forms\Components\Grid::make([ + 'default' => 1, + ]) + ->hidden(fn (Forms\Get $get) => $get('wecom_enabled') !== true) + ->schema([ + Forms\Components\Fieldset::make('Triggers') + ->schema([ + Forms\Components\Toggle::make('wecom_on_speedtest_run') + ->label('Notify on every speedtest run') + ->columnSpan(2), + Forms\Components\Toggle::make('wecom_on_threshold_failure') + ->label('Notify on threshold failures') + ->columnSpan(2), + ]), + Forms\Components\Repeater::make('wecom_webhooks') + ->label('Webhooks') + ->schema([ + Forms\Components\TextInput::make('url') + ->placeholder('https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx') + ->maxLength(2000) + ->required() + ->url(), + ]) + ->columnSpanFull(), + Forms\Components\Actions::make([ + Forms\Components\Actions\Action::make('test wecom') + ->label('Test wecom channel') + ->action(fn (Forms\Get $get) => SendWeComTestNotification::run(webhooks: $get('wecom_webhooks'))) + ->hidden(fn (Forms\Get $get) => ! count($get('wecom_webhooks'))), + ]), + ]), + ]) + ->compact() + ->columns([ + 'default' => 1, + 'md' => 2, + ]), + ]) ->columnSpan([ 'md' => 2, diff --git a/app/Listeners/DingTalk/SendSpeedtestCompletedNotification.php b/app/Listeners/DingTalk/SendSpeedtestCompletedNotification.php new file mode 100644 index 000000000..da00978cd --- /dev/null +++ b/app/Listeners/DingTalk/SendSpeedtestCompletedNotification.php @@ -0,0 +1,63 @@ +dingtalk_enabled) { + return; + } + + if (! $notificationSettings->dingtalk_on_speedtest_run) { + return; + } + + if (! count($notificationSettings->dingtalk_webhooks)) { + Log::warning('DingTalk urls not found, check dingtalk notification channel settings.'); + + return; + } + + $payload = [ + 'msgtype' => 'markdown', + 'markdown' => [ + 'title' => sprintf('Speedtest Completed - #%s', $event->result->id), + 'text' => view('dingtalk.speedtest-completed', [ + 'id' => $event->result->id, + 'service' => Str::title($event->result->service->getLabel()), + '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' => is_numeric($event->result->packet_loss) ? round($event->result->packet_loss, 2) : 'n/a', + 'speedtest_url' => $event->result->result_url, + 'url' => url('/admin/results'), + ])->render(), + ], + ]; + + foreach ($notificationSettings->dingtalk_webhooks as $webhook) { + WebhookCall::create() + ->url($webhook['url']) + ->payload($payload) + ->doNotSign() + ->dispatch(); + } + } +} diff --git a/app/Listeners/DingTalk/SendSpeedtestThresholdNotification.php b/app/Listeners/DingTalk/SendSpeedtestThresholdNotification.php new file mode 100644 index 000000000..e17979b59 --- /dev/null +++ b/app/Listeners/DingTalk/SendSpeedtestThresholdNotification.php @@ -0,0 +1,137 @@ +dingtalk_enabled) { + return; + } + + if (! $notificationSettings->dingtalk_on_threshold_failure) { + return; + } + + if (! count($notificationSettings->dingtalk_webhooks)) { + Log::warning('DingTalk urls not found, check dingtalk 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 dingtalk thresholds not found, won\'t send notification.'); + + return; + } + + $payload = [ + 'msgtype' => 'markdown', + 'markdown' => [ + 'title' => sprintf('Speedtest Threshold Breached - #%s', $event->result->id), + 'text' => view('dingtalk.speedtest-threshold', [ + 'id' => $event->result->id, + 'service' => Str::title($event->result->service->getLabel()), + 'serverName' => $event->result->server_name, + 'serverId' => $event->result->server_id, + 'isp' => $event->result->isp, + 'metrics' => $failed, + 'speedtest_url' => $event->result->result_url, + 'url' => url('/admin/results'), + ])->render(), + ], + ]; + + foreach ($notificationSettings->dingtalk_webhooks as $webhook) { + WebhookCall::create() + ->url($webhook['url']) + ->payload($payload) + ->doNotSign() + ->dispatch(); + } + } + + /** + * Build webhook 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 webhook 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 webhook 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/Listeners/WeCom/SendSpeedtestCompletedNotification.php b/app/Listeners/WeCom/SendSpeedtestCompletedNotification.php new file mode 100644 index 000000000..c11052a83 --- /dev/null +++ b/app/Listeners/WeCom/SendSpeedtestCompletedNotification.php @@ -0,0 +1,62 @@ +wecom_enabled) { + return; + } + + if (! $notificationSettings->wecom_on_speedtest_run) { + return; + } + + if (! count($notificationSettings->wecom_webhooks)) { + Log::warning('WeCom urls not found, check wecom notification channel settings.'); + + return; + } + + $payload = [ + 'msgtype' => 'markdown', + 'markdown' => [ + 'content' => view('wecom.speedtest-completed', [ + 'id' => $event->result->id, + 'service' => Str::title($event->result->service->getLabel()), + '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' => is_numeric($event->result->packet_loss) ? round($event->result->packet_loss, 2) : 'n/a', + 'speedtest_url' => $event->result->result_url, + 'url' => url('/admin/results'), + ])->render(), + ], + ]; + + foreach ($notificationSettings->wecom_webhooks as $webhook) { + WebhookCall::create() + ->url($webhook['url']) + ->payload($payload) + ->doNotSign() + ->dispatch(); + } + } +} diff --git a/app/Listeners/WeCom/SendSpeedtestThresholdNotification.php b/app/Listeners/WeCom/SendSpeedtestThresholdNotification.php new file mode 100644 index 000000000..a16add98b --- /dev/null +++ b/app/Listeners/WeCom/SendSpeedtestThresholdNotification.php @@ -0,0 +1,136 @@ +wecom_enabled) { + return; + } + + if (! $notificationSettings->wecom_on_threshold_failure) { + return; + } + + if (! count($notificationSettings->wecom_webhooks)) { + Log::warning('WeCom urls not found, check wecom 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 wecom thresholds not found, won\'t send notification.'); + + return; + } + + $payload = [ + 'msgtype' => 'markdown', + 'markdown' => [ + 'content' => view('wecom.speedtest-threshold', [ + 'id' => $event->result->id, + 'service' => Str::title($event->result->service->getLabel()), + 'serverName' => $event->result->server_name, + 'serverId' => $event->result->server_id, + 'isp' => $event->result->isp, + 'metrics' => $failed, + 'speedtest_url' => $event->result->result_url, + 'url' => url('/admin/results'), + ])->render(), + ], + ]; + + foreach ($notificationSettings->wecom_webhooks as $webhook) { + WebhookCall::create() + ->url($webhook['url']) + ->payload($payload) + ->doNotSign() + ->dispatch(); + } + } + + /** + * Build webhook 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 webhook 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 webhook 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 0796be61a..148ffaefb 100644 --- a/app/Settings/NotificationSettings.php +++ b/app/Settings/NotificationSettings.php @@ -86,6 +86,22 @@ class NotificationSettings extends Settings public ?array $gotify_webhooks; + public bool $dingtalk_enabled; + + public bool $dingtalk_on_speedtest_run; + + public bool $dingtalk_on_threshold_failure; + + public ?array $dingtalk_webhooks; + + public bool $wecom_enabled; + + public bool $wecom_on_speedtest_run; + + public bool $wecom_on_threshold_failure; + + public ?array $wecom_webhooks; + public static function group(): string { return 'notification'; diff --git a/database/settings/2025_04_08_034447_create_dingtalk_notification_settings.php b/database/settings/2025_04_08_034447_create_dingtalk_notification_settings.php new file mode 100644 index 000000000..65d9ff33f --- /dev/null +++ b/database/settings/2025_04_08_034447_create_dingtalk_notification_settings.php @@ -0,0 +1,14 @@ +migrator->add('notification.dingtalk_enabled', false); + $this->migrator->add('notification.dingtalk_on_speedtest_run', false); + $this->migrator->add('notification.dingtalk_on_threshold_failure', false); + $this->migrator->add('notification.dingtalk_webhooks', null); + } +}; diff --git a/database/settings/2025_04_08_065526_create_wecom_notification_settings.php b/database/settings/2025_04_08_065526_create_wecom_notification_settings.php new file mode 100644 index 000000000..09378b155 --- /dev/null +++ b/database/settings/2025_04_08_065526_create_wecom_notification_settings.php @@ -0,0 +1,14 @@ +migrator->add('notification.wecom_enabled', false); + $this->migrator->add('notification.wecom_on_speedtest_run', false); + $this->migrator->add('notification.wecom_on_threshold_failure', false); + $this->migrator->add('notification.wecom_webhooks', null); + } +}; diff --git a/resources/views/dingtalk/speedtest-completed.blade.php b/resources/views/dingtalk/speedtest-completed.blade.php new file mode 100644 index 000000000..497c3bf12 --- /dev/null +++ b/resources/views/dingtalk/speedtest-completed.blade.php @@ -0,0 +1,13 @@ +*Speedtest Completed - #{{ $id }}* + +A new speedtest on *{{ config('app.name') }}* was completed using *{{ $service }}*. + +- *Server name:* {{ $serverName }} +- *Server ID:* {{ $serverId }} +- **ISP:** {{ $isp }} +- *Ping:* {{ $ping }} +- *Download:* {{ $download }} +- *Upload:* {{ $upload }} +- **Packet Loss:** {{ $packetLoss }}**%** +- **Ookla Speedtest:** {{ $speedtest_url }} +- **URL:** {{ $url }} diff --git a/resources/views/dingtalk/speedtest-threshold.blade.php b/resources/views/dingtalk/speedtest-threshold.blade.php new file mode 100644 index 000000000..95dc4bf01 --- /dev/null +++ b/resources/views/dingtalk/speedtest-threshold.blade.php @@ -0,0 +1,9 @@ +**Speedtest Threshold Breached - #{{ $id }}** + +A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. + +@foreach ($metrics as $item) +- **{{ $item['name'] }}** {{ $item['threshold'] }}: {{ $item['value'] }} +@endforeach +- **Ookla Speedtest:** {{ $speedtest_url }} +- **URL:** {{ $url }} diff --git a/resources/views/wecom/speedtest-completed.blade.php b/resources/views/wecom/speedtest-completed.blade.php new file mode 100644 index 000000000..497c3bf12 --- /dev/null +++ b/resources/views/wecom/speedtest-completed.blade.php @@ -0,0 +1,13 @@ +*Speedtest Completed - #{{ $id }}* + +A new speedtest on *{{ config('app.name') }}* was completed using *{{ $service }}*. + +- *Server name:* {{ $serverName }} +- *Server ID:* {{ $serverId }} +- **ISP:** {{ $isp }} +- *Ping:* {{ $ping }} +- *Download:* {{ $download }} +- *Upload:* {{ $upload }} +- **Packet Loss:** {{ $packetLoss }}**%** +- **Ookla Speedtest:** {{ $speedtest_url }} +- **URL:** {{ $url }} diff --git a/resources/views/wecom/speedtest-threshold.blade.php b/resources/views/wecom/speedtest-threshold.blade.php new file mode 100644 index 000000000..95dc4bf01 --- /dev/null +++ b/resources/views/wecom/speedtest-threshold.blade.php @@ -0,0 +1,9 @@ +**Speedtest Threshold Breached - #{{ $id }}** + +A new speedtest on **{{ config('app.name') }}** was completed using **{{ $service }}** on **{{ $isp }}** but a threshold was breached. + +@foreach ($metrics as $item) +- **{{ $item['name'] }}** {{ $item['threshold'] }}: {{ $item['value'] }} +@endforeach +- **Ookla Speedtest:** {{ $speedtest_url }} +- **URL:** {{ $url }}