Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 66 additions & 11 deletions app/Actions/Notifications/SendAppriseTestNotification.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -22,24 +24,77 @@ 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()
->title('Test Apprise notification sent.')
->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;
}
}
5 changes: 4 additions & 1 deletion app/Filament/Pages/Settings/Notification.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'))
Expand Down
33 changes: 17 additions & 16 deletions app/Notifications/AppriseChannel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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');
Expand All @@ -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,
Expand All @@ -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;
}
}
}
24 changes: 24 additions & 0 deletions app/Rules/ContainsString.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class ContainsString implements ValidationRule
{
public function __construct(
protected string $needle,
protected bool $caseSensitive = false
) {}

public function validate(string $attribute, mixed $value, Closure $fail): void
{
$haystack = $this->caseSensitive ? $value : strtolower($value);
$needle = $this->caseSensitive ? $this->needle : strtolower($this->needle);

if (! str_contains($haystack, $needle)) {
$fail("The :attribute must contain '{$this->needle}'.");
}
}
}
13 changes: 7 additions & 6 deletions lang/en/settings/notifications.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down