Skip to content

Commit 17bdf73

Browse files
feat: Apprise notification channel (#2415)
Co-authored-by: Alex Justesen <[email protected]>
1 parent c9935b2 commit 17bdf73

File tree

17 files changed

+545
-30
lines changed

17 files changed

+545
-30
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace App\Actions\Notifications;
4+
5+
use App\Notifications\Apprise\TestNotification;
6+
use Filament\Notifications\Notification;
7+
use Illuminate\Support\Facades\Notification as FacadesNotification;
8+
use Lorisleiva\Actions\Concerns\AsAction;
9+
10+
class SendAppriseTestNotification
11+
{
12+
use AsAction;
13+
14+
public function handle(array $channel_urls)
15+
{
16+
if (! count($channel_urls)) {
17+
Notification::make()
18+
->title('You need to add Apprise channel URLs!')
19+
->warning()
20+
->send();
21+
22+
return;
23+
}
24+
25+
foreach ($channel_urls as $row) {
26+
$channelUrl = $row['channel_url'] ?? null;
27+
if (! $channelUrl) {
28+
Notification::make()
29+
->title('Skipping missing channel URL!')
30+
->warning()
31+
->send();
32+
33+
continue;
34+
}
35+
36+
FacadesNotification::route('apprise_urls', $channelUrl)
37+
->notify(new TestNotification);
38+
}
39+
40+
Notification::make()
41+
->title('Test Apprise notification sent.')
42+
->success()
43+
->send();
44+
}
45+
}

app/Filament/Pages/Settings/Notification.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Filament\Pages\Settings;
44

5+
use App\Actions\Notifications\SendAppriseTestNotification;
56
use App\Actions\Notifications\SendDatabaseTestNotification;
67
use App\Actions\Notifications\SendDiscordTestNotification;
78
use App\Actions\Notifications\SendGotifyTestNotification;
@@ -12,6 +13,7 @@
1213
use App\Actions\Notifications\SendSlackTestNotification;
1314
use App\Actions\Notifications\SendTelegramTestNotification;
1415
use App\Actions\Notifications\SendWebhookTestNotification;
16+
use App\Rules\AppriseScheme;
1517
use App\Settings\NotificationSettings;
1618
use CodeWithDennis\SimpleAlert\Components\SimpleAlert;
1719
use Filament\Actions\Action;
@@ -199,6 +201,80 @@ public function form(Schema $schema): Schema
199201

200202
// ...
201203
]),
204+
Tab::make(__('settings/notifications.apprise'))
205+
->icon(Heroicon::CloudArrowUp)
206+
->schema([
207+
SimpleAlert::make('wehbook_info')
208+
->title(__('general.documentation'))
209+
->description(__('settings/notifications.apprise_hint_description'))
210+
->border()
211+
->info()
212+
->actions([
213+
Action::make('webhook_docs')
214+
->label(__('general.view_documentation'))
215+
->icon('heroicon-m-arrow-long-right')
216+
->color('info')
217+
->link()
218+
->url('https://docs.speedtest-tracker.dev/settings/notifications/apprise')
219+
->openUrlInNewTab(),
220+
])
221+
->columnSpanFull(),
222+
223+
Toggle::make('apprise_enabled')
224+
->label(__('settings/notifications.enable_apprise_notifications'))
225+
->reactive()
226+
->columnSpanFull(),
227+
Grid::make([
228+
'default' => 1,
229+
])
230+
->hidden(fn (Get $get) => $get('apprise_enabled') !== true)
231+
->schema([
232+
Fieldset::make(__('settings/notifications.apprise_server'))
233+
->schema([
234+
TextInput::make('apprise_server_url')
235+
->label(__('settings/notifications.apprise_server_url'))
236+
->placeholder('http://localhost:8000')
237+
->maxLength(2000)
238+
->required()
239+
->url()
240+
->columnSpanFull(),
241+
Checkbox::make('apprise_verify_ssl')
242+
->label(__('settings/notifications.apprise_verify_ssl'))
243+
->default(true)
244+
->columnSpanFull(),
245+
]),
246+
Fieldset::make(__('settings.triggers'))
247+
->schema([
248+
Checkbox::make('apprise_on_speedtest_run')
249+
->label(__('settings/notifications.notify_on_every_speedtest_run'))
250+
->columnSpanFull(),
251+
Checkbox::make('apprise_on_threshold_failure')
252+
->label(__('settings/notifications.notify_on_threshold_failures'))
253+
->columnSpanFull(),
254+
]),
255+
Repeater::make('apprise_channel_urls')
256+
->label(__('settings/notifications.apprise_channels'))
257+
->schema([
258+
TextInput::make('channel_url')
259+
->label(__('settings/notifications.apprise_channel_url'))
260+
->placeholder('discord://WebhookID/WebhookToken')
261+
->helperText(__('settings/notifications.apprise_channel_url_helper'))
262+
->maxLength(2000)
263+
->distinct()
264+
->required()
265+
->rule(new AppriseScheme),
266+
])
267+
->columnSpanFull(),
268+
Actions::make([
269+
Action::make('test apprise')
270+
->label(__('settings/notifications.test_apprise_channel'))
271+
->action(fn (Get $get) => SendAppriseTestNotification::run(
272+
channel_urls: $get('apprise_channel_urls'),
273+
))
274+
->hidden(fn (Get $get) => ! count($get('apprise_channel_urls'))),
275+
]),
276+
]),
277+
]),
202278
])
203279
->columnSpanFull(),
204280

app/Listeners/ProcessCompletedSpeedtest.php

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@
33
namespace App\Listeners;
44

55
use App\Events\SpeedtestCompleted;
6+
use App\Helpers\Number;
67
use App\Mail\CompletedSpeedtestMail;
78
use App\Models\Result;
89
use App\Models\User;
10+
use App\Notifications\Apprise\SpeedtestNotification;
911
use App\Settings\NotificationSettings;
1012
use Filament\Actions\Action;
11-
use Filament\Notifications\Notification;
13+
use Filament\Notifications\Notification as FilamentNotification;
1214
use Illuminate\Support\Arr;
1315
use Illuminate\Support\Facades\Log;
1416
use Illuminate\Support\Facades\Mail;
17+
use Illuminate\Support\Facades\Notification;
18+
use Illuminate\Support\Str;
1519
use Spatie\WebhookServer\WebhookCall;
1620

1721
class ProcessCompletedSpeedtest
@@ -29,7 +33,7 @@ public function handle(SpeedtestCompleted $event): void
2933

3034
$result->loadMissing(['dispatchedBy']);
3135

32-
// $this->notifyAppriseChannels($result);
36+
$this->notifyAppriseChannels($result);
3337
$this->notifyDatabaseChannels($result);
3438
$this->notifyDispatchingUser($result);
3539
$this->notifyMailChannels($result);
@@ -42,11 +46,50 @@ public function handle(SpeedtestCompleted $event): void
4246
private function notifyAppriseChannels(Result $result): void
4347
{
4448
// Don't send Apprise notification if dispatched by a user or test is unhealthy.
45-
if (filled($result->dispatched_by) || ! $result->healthy) {
49+
if (filled($result->dispatched_by) || $result->healthy === false) {
4650
return;
4751
}
4852

49-
//
53+
// Check if Apprise notifications are enabled.
54+
if (! $this->notificationSettings->apprise_enabled || ! $this->notificationSettings->apprise_on_speedtest_run) {
55+
return;
56+
}
57+
58+
if (! count($this->notificationSettings->apprise_channel_urls)) {
59+
Log::warning('Apprise channel URLs not found, check Apprise notification channel settings.');
60+
61+
return;
62+
}
63+
64+
// Build the speedtest data
65+
$body = view('apprise.speedtest-completed', [
66+
'id' => $result->id,
67+
'service' => Str::title($result->service->getLabel()),
68+
'serverName' => $result->server_name,
69+
'serverId' => $result->server_id,
70+
'isp' => $result->isp,
71+
'ping' => round($result->ping).' ms',
72+
'download' => Number::toBitRate(bits: $result->download_bits, precision: 2),
73+
'upload' => Number::toBitRate(bits: $result->upload_bits, precision: 2),
74+
'packetLoss' => $result->packet_loss,
75+
'speedtest_url' => $result->result_url,
76+
'url' => url('/admin/results'),
77+
])->render();
78+
79+
$title = 'Speedtest Completed – #'.$result->id;
80+
81+
// Send notification to each configured channel URL
82+
foreach ($this->notificationSettings->apprise_channel_urls as $row) {
83+
$channelUrl = $row['channel_url'] ?? null;
84+
if (! $channelUrl) {
85+
Log::warning('Skipping entry with missing channel_url.');
86+
87+
continue;
88+
}
89+
90+
Notification::route('apprise_urls', $channelUrl)
91+
->notify(new SpeedtestNotification($title, $body, 'info'));
92+
}
5093
}
5194

5295
/**
@@ -65,7 +108,7 @@ private function notifyDatabaseChannels(Result $result): void
65108
}
66109

67110
foreach (User::all() as $user) {
68-
Notification::make()
111+
FilamentNotification::make()
69112
->title(__('results.speedtest_completed'))
70113
->actions([
71114
Action::make('view')
@@ -87,7 +130,7 @@ private function notifyDispatchingUser(Result $result): void
87130
}
88131

89132
$result->dispatchedBy->notify(
90-
Notification::make()
133+
FilamentNotification::make()
91134
->title(__('results.speedtest_completed'))
92135
->actions([
93136
Action::make('view')

app/Listeners/ProcessUnhealthySpeedtest.php

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
namespace App\Listeners;
44

55
use App\Events\SpeedtestBenchmarkFailed;
6+
use App\Helpers\Number;
67
use App\Mail\UnhealthySpeedtestMail;
78
use App\Models\Result;
89
use App\Models\User;
10+
use App\Notifications\Apprise\SpeedtestNotification;
911
use App\Settings\NotificationSettings;
1012
use Filament\Actions\Action;
11-
use Filament\Notifications\Notification;
13+
use Filament\Notifications\Notification as FilamentNotification;
1214
use Illuminate\Support\Facades\Log;
1315
use Illuminate\Support\Facades\Mail;
16+
use Illuminate\Support\Facades\Notification;
17+
use Illuminate\Support\Str;
1418
use Spatie\WebhookServer\WebhookCall;
1519

1620
class ProcessUnhealthySpeedtest
@@ -31,7 +35,7 @@ public function handle(SpeedtestBenchmarkFailed $event): void
3135

3236
$result->loadMissing(['dispatchedBy']);
3337

34-
// $this->notifyAppriseChannels($result);
38+
$this->notifyAppriseChannels($result);
3539
$this->notifyDatabaseChannels($result);
3640
$this->notifyDispatchingUser($result);
3741
$this->notifyMailChannels($result);
@@ -48,7 +52,79 @@ private function notifyAppriseChannels(Result $result): void
4852
return;
4953
}
5054

51-
//
55+
if (! $this->notificationSettings->apprise_enabled || ! $this->notificationSettings->apprise_on_threshold_failure) {
56+
return;
57+
}
58+
59+
if (! count($this->notificationSettings->apprise_channel_urls)) {
60+
Log::warning('Apprise channel URLs not found, check Apprise notification channel settings.');
61+
62+
return;
63+
}
64+
65+
if (empty($result->benchmarks)) {
66+
Log::warning('Benchmark data not found, won\'t send Apprise notification.');
67+
68+
return;
69+
}
70+
71+
// Build metrics array from failed benchmarks
72+
$failed = [];
73+
74+
foreach ($result->benchmarks as $metric => $benchmark) {
75+
if ($benchmark['passed'] === false) {
76+
$failed[] = [
77+
'name' => ucfirst($metric),
78+
'threshold' => $benchmark['value'].' '.$benchmark['unit'],
79+
'value' => $this->formatMetricValue($metric, $result),
80+
];
81+
}
82+
}
83+
84+
if (! count($failed)) {
85+
Log::warning('No failed thresholds found in benchmarks, won\'t send Apprise notification.');
86+
87+
return;
88+
}
89+
90+
$body = view('apprise.speedtest-threshold', [
91+
'id' => $result->id,
92+
'service' => Str::title($result->service->getLabel()),
93+
'serverName' => $result->server_name,
94+
'serverId' => $result->server_id,
95+
'isp' => $result->isp,
96+
'metrics' => $failed,
97+
'speedtest_url' => $result->result_url,
98+
'url' => url('/admin/results'),
99+
])->render();
100+
101+
$title = 'Speedtest Threshold Breach – #'.$result->id;
102+
103+
// Send notification to each configured channel URL
104+
foreach ($this->notificationSettings->apprise_channel_urls as $row) {
105+
$channelUrl = $row['channel_url'] ?? null;
106+
if (! $channelUrl) {
107+
Log::warning('Skipping entry with missing channel_url.');
108+
109+
continue;
110+
}
111+
112+
Notification::route('apprise_urls', $channelUrl)
113+
->notify(new SpeedtestNotification($title, $body, 'warning'));
114+
}
115+
}
116+
117+
/**
118+
* Format metric value for display in notification.
119+
*/
120+
private function formatMetricValue(string $metric, Result $result): string
121+
{
122+
return match ($metric) {
123+
'download' => Number::toBitRate(bits: $result->download_bits, precision: 2),
124+
'upload' => Number::toBitRate(bits: $result->upload_bits, precision: 2),
125+
'ping' => round($result->ping, 2).' ms',
126+
default => '',
127+
};
52128
}
53129

54130
/**
@@ -67,7 +143,7 @@ private function notifyDatabaseChannels(Result $result): void
67143
}
68144

69145
foreach (User::all() as $user) {
70-
Notification::make()
146+
FilamentNotification::make()
71147
->title(__('results.speedtest_benchmark_failed'))
72148
->actions([
73149
Action::make('view')
@@ -89,7 +165,7 @@ private function notifyDispatchingUser(Result $result): void
89165
}
90166

91167
$result->dispatchedBy->notify(
92-
Notification::make()
168+
FilamentNotification::make()
93169
->title(__('results.speedtest_benchmark_failed'))
94170
->actions([
95171
Action::make('view')
@@ -106,7 +182,7 @@ private function notifyDispatchingUser(Result $result): void
106182
*/
107183
private function notifyMailChannels(Result $result): void
108184
{
109-
// Don't send webhook if dispatched by a user.
185+
// Don't send mail if dispatched by a user.
110186
if (filled($result->dispatched_by)) {
111187
return;
112188
}

0 commit comments

Comments
 (0)