diff --git a/app/Actions/Notifications/SendAppriseTestNotification.php b/app/Actions/Notifications/SendAppriseTestNotification.php index 062202b06..64bba779b 100644 --- a/app/Actions/Notifications/SendAppriseTestNotification.php +++ b/app/Actions/Notifications/SendAppriseTestNotification.php @@ -3,15 +3,17 @@ namespace App\Actions\Notifications; use App\Notifications\Apprise\TestNotification; +use App\Settings\NotificationSettings; use Filament\Notifications\Notification; use Illuminate\Support\Facades\Notification as FacadesNotification; use Lorisleiva\Actions\Concerns\AsAction; +use Throwable; class SendAppriseTestNotification { use AsAction; - public function handle(array $channel_urls) + public function handle(array $channel_urls): void { if (! count($channel_urls)) { Notification::make() @@ -22,19 +24,41 @@ public function handle(array $channel_urls) return; } - foreach ($channel_urls as $row) { - $channelUrl = $row['channel_url'] ?? null; - if (! $channelUrl) { - Notification::make() - ->title('Skipping missing channel URL!') - ->warning() - ->send(); + $settings = app(NotificationSettings::class); + $appriseUrl = rtrim($settings->apprise_server_url ?? '', '/'); - continue; + if (empty($appriseUrl)) { + Notification::make() + ->title('Apprise Server URL is not configured') + ->body('Please configure the Apprise Server URL in the settings above.') + ->danger() + ->send(); + + return; + } + + try { + foreach ($channel_urls as $row) { + $channelUrl = $row['channel_url'] ?? null; + if (! $channelUrl) { + continue; + } + + // Use notifyNow() to send synchronously even though notification implements ShouldQueue + // This allows us to catch exceptions and show them in the UI immediately + FacadesNotification::route('apprise_urls', $channelUrl) + ->notifyNow(new TestNotification); } + } catch (Throwable $e) { + $errorMessage = $this->cleanErrorMessage($e); + + Notification::make() + ->title('Failed to send Apprise test notification') + ->body($errorMessage) + ->danger() + ->send(); - FacadesNotification::route('apprise_urls', $channelUrl) - ->notify(new TestNotification); + return; } Notification::make() @@ -42,4 +66,35 @@ public function handle(array $channel_urls) ->success() ->send(); } + + /** + * Clean up error message for display in UI. + */ + protected function cleanErrorMessage(Throwable $e): string + { + $message = $e->getMessage(); + + // Get the full Apprise server URL for error messages + $settings = app(NotificationSettings::class); + $appriseUrl = rtrim($settings->apprise_server_url ?? '', '/'); + + // Handle connection errors - extract just the important part + if (str_contains($message, 'cURL error')) { + if (str_contains($message, 'Could not resolve host')) { + return "Could not connect to Apprise server at {$appriseUrl}"; + } + + if (str_contains($message, 'Connection refused')) { + return "Connection refused by Apprise server at {$appriseUrl}"; + } + + if (str_contains($message, 'Operation timed out')) { + return "Connection to Apprise server at {$appriseUrl} timed out"; + } + + return "Failed to connect to Apprise server at {$appriseUrl}"; + } + + return $message; + } } diff --git a/app/Filament/Pages/Settings/Notification.php b/app/Filament/Pages/Settings/Notification.php index 9a5decb00..7e1c1a58a 100755 --- a/app/Filament/Pages/Settings/Notification.php +++ b/app/Filament/Pages/Settings/Notification.php @@ -14,6 +14,7 @@ use App\Actions\Notifications\SendTelegramTestNotification; use App\Actions\Notifications\SendWebhookTestNotification; use App\Rules\AppriseScheme; +use App\Rules\ContainsString; use App\Settings\NotificationSettings; use CodeWithDennis\SimpleAlert\Components\SimpleAlert; use Filament\Actions\Action; @@ -233,10 +234,12 @@ public function form(Schema $schema): Schema ->schema([ TextInput::make('apprise_server_url') ->label(__('settings/notifications.apprise_server_url')) - ->placeholder('http://localhost:8000') + ->placeholder('http://localhost:8000/notify') + ->helperText(__('settings/notifications.apprise_server_url_helper')) ->maxLength(2000) ->required() ->url() + ->rule(new ContainsString('/notify')) ->columnSpanFull(), Checkbox::make('apprise_verify_ssl') ->label(__('settings/notifications.apprise_verify_ssl')) diff --git a/app/Notifications/AppriseChannel.php b/app/Notifications/AppriseChannel.php index 3cd2592a1..0e9ed1087 100644 --- a/app/Notifications/AppriseChannel.php +++ b/app/Notifications/AppriseChannel.php @@ -3,9 +3,11 @@ namespace App\Notifications; use App\Settings\NotificationSettings; +use Exception; use Illuminate\Notifications\Notification; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use Throwable; class AppriseChannel { @@ -22,7 +24,7 @@ public function send(object $notifiable, Notification $notification): void } $settings = app(NotificationSettings::class); - $appriseUrl = rtrim($settings->apprise_server_url ?? '', '/'); + $appriseUrl = $settings->apprise_server_url ?? ''; if (empty($appriseUrl)) { Log::warning('Apprise notification skipped: No Server URL configured'); @@ -41,7 +43,7 @@ public function send(object $notifiable, Notification $notification): void $request = $request->withoutVerifying(); } - $response = $request->post("{$appriseUrl}/notify", [ + $response = $request->post($appriseUrl, [ 'urls' => $message->urls, 'title' => $message->title, 'body' => $message->body, @@ -50,26 +52,25 @@ public function send(object $notifiable, Notification $notification): void 'tag' => $message->tag ?? null, ]); - if ($response->failed()) { - Log::error('Apprise notification failed', [ - 'channel' => $message->urls, - 'instance' => $appriseUrl, - 'status' => $response->status(), - 'body' => $response->body(), - ]); - } else { - Log::info('Apprise notification sent', [ - 'channel' => $message->urls, - 'instance' => $appriseUrl, - ]); + // Only accept 200 OK responses as successful + if ($response->status() !== 200) { + throw new Exception('Apprise returned an error, please check Apprise logs for details'); } - } catch (\Throwable $e) { - Log::error('Apprise notification exception', [ + + Log::info('Apprise notification sent', [ + 'channel' => $message->urls, + 'instance' => $appriseUrl, + ]); + } catch (Throwable $e) { + Log::error('Apprise notification failed', [ 'channel' => $message->urls ?? 'unknown', 'instance' => $appriseUrl, 'message' => $e->getMessage(), 'exception' => get_class($e), ]); + + // Re-throw the exception so it can be handled by the queue + throw $e; } } } diff --git a/app/Rules/ContainsString.php b/app/Rules/ContainsString.php new file mode 100644 index 000000000..370a499c6 --- /dev/null +++ b/app/Rules/ContainsString.php @@ -0,0 +1,24 @@ +caseSensitive ? $value : strtolower($value); + $needle = $this->caseSensitive ? $this->needle : strtolower($this->needle); + + if (! str_contains($haystack, $needle)) { + $fail("The :attribute must contain '{$this->needle}'."); + } + } +} diff --git a/lang/en/settings/notifications.php b/lang/en/settings/notifications.php index 203590b23..788f31d91 100644 --- a/lang/en/settings/notifications.php +++ b/lang/en/settings/notifications.php @@ -19,19 +19,20 @@ 'enable_apprise_notifications' => 'Enable Apprise notifications', 'apprise_server' => 'Apprise Server', 'apprise_server_url' => 'Apprise Server URL', + 'apprise_server_url_helper' => 'The URL of your Apprise Server. The URL must end on /notify', 'apprise_verify_ssl' => 'Verify SSL', - 'apprise_channels' => 'Apprise Channels', - 'apprise_channel_url' => 'Channel URL', - 'apprise_hint_description' => 'For more information on setting up Apprise, view the documentation.', - 'apprise_channel_url_helper' => 'Provide the service endpoint URL for notifications.', + 'apprise_channels' => 'Notification Channels', + 'apprise_channel_url' => 'Service URL', + 'apprise_hint_description' => 'Apprise allows you to send notifications to 90+ services. You need to run an Apprise server and configure service URLs below.', + 'apprise_channel_url_helper' => 'Use Apprise URL format. Examples: discord://WebhookID/Token, slack://TokenA/TokenB/TokenC', 'test_apprise_channel' => 'Test Apprise', - 'apprise_channel_url_validation_error' => 'The Apprise channel URL must not start with "http" or "https". Please provide a valid Apprise URL scheme.', + 'apprise_channel_url_validation_error' => 'Invalid Apprise URL. Must use Apprise format (e.g., discord://, slack://), not http:// or https://. See the Apprise documentation for more information', // Webhook 'webhook' => 'Webhook', 'webhooks' => 'Webhooks', 'test_webhook_channel' => 'Test webhook channel', - 'webhook_hint_description' => 'These are generic webhooks. For payload examples and implementation details, view the documentation.', + 'webhook_hint_description' => 'These are generic webhooks. For payload examples and implementation details, view the documentation. For services like Discord, Ntfy etc please use Apprise.', // Common notification messages 'notify_on_every_speedtest_run' => 'Notify on every scheduled speedtest run',