Skip to content

Commit a49e395

Browse files
authored
[Feature] Add Apprise (alexjustesen#2384)
1 parent 09cbc50 commit a49e395

18 files changed

+1122
-436
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+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Jobs\Notifications\Apprise\SendSpeedtestCompletedNotification as AppriseCompleted;
6+
use App\Jobs\Notifications\Apprise\SendSpeedtestThresholdNotification as AppriseThreshold;
7+
use App\Models\Result;
8+
use Illuminate\Console\Command;
9+
10+
class TestNotification extends Command
11+
{
12+
protected $signature = 'notification:test
13+
{type=completed : Notification type (completed or threshold)}
14+
{--channel=apprise : Notification channel (apprise)}';
15+
16+
protected $description = 'Send a test notification using a fake result';
17+
18+
public function handle(): int
19+
{
20+
$type = $this->argument('type');
21+
$channel = $this->option('channel');
22+
23+
$this->info("Creating fake result for type: {$type}");
24+
25+
$result = Result::factory()->create([
26+
'status' => 'completed',
27+
]);
28+
29+
$this->info("Dispatching {$channel} notification...");
30+
31+
match ("{$channel}-{$type}") {
32+
'apprise-completed' => AppriseCompleted::dispatch($result),
33+
'apprise-threshold' => AppriseThreshold::dispatch($result),
34+
};
35+
36+
$this->info('✅ Notification dispatched!');
37+
38+
return self::SUCCESS;
39+
}
40+
}

app/Filament/Pages/Settings/NotificationPage.php

Lines changed: 503 additions & 423 deletions
Large diffs are not rendered by default.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace App\Listeners\Apprise;
4+
5+
use App\Events\SpeedtestCompleted;
6+
use App\Notifications\Apprise\SpeedtestNotification;
7+
use App\Services\Notifications\SpeedtestNotificationData;
8+
use App\Settings\NotificationSettings;
9+
use Illuminate\Support\Facades\Log;
10+
use Illuminate\Support\Facades\Notification;
11+
12+
class SendSpeedtestCompletedNotification
13+
{
14+
/**
15+
* Handle the event.
16+
*/
17+
public function handle(SpeedtestCompleted $event): void
18+
{
19+
$notificationSettings = new NotificationSettings;
20+
21+
if (! $notificationSettings->apprise_enabled) {
22+
return;
23+
}
24+
25+
if (! $notificationSettings->apprise_on_speedtest_run) {
26+
return;
27+
}
28+
29+
if (empty($notificationSettings->apprise_channel_urls) || ! is_array($notificationSettings->apprise_channel_urls)) {
30+
Log::warning('Apprise service URLs not found; check Apprise notification settings.');
31+
32+
return;
33+
}
34+
35+
// Build the speedtest data
36+
$data = SpeedtestNotificationData::make($event->result);
37+
38+
$body = view('apprise.speedtest-completed', $data)->render();
39+
$title = 'Speedtest Completed – #'.$event->result->id;
40+
41+
// Send notification to each configured channel URL
42+
foreach ($notificationSettings->apprise_channel_urls as $row) {
43+
$channelUrl = $row['channel_url'] ?? null;
44+
if (! $channelUrl) {
45+
Log::warning('Skipping entry with missing channel_url.');
46+
47+
continue;
48+
}
49+
50+
Notification::route('apprise_urls', $channelUrl)
51+
->notify(new SpeedtestNotification($title, $body, 'info'));
52+
}
53+
}
54+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
namespace App\Listeners\Apprise;
4+
5+
use App\Events\SpeedtestCompleted;
6+
use App\Helpers\Number;
7+
use App\Notifications\Apprise\SpeedtestNotification;
8+
use App\Settings\NotificationSettings;
9+
use App\Settings\ThresholdSettings;
10+
use Illuminate\Support\Facades\Log;
11+
use Illuminate\Support\Facades\Notification;
12+
use Illuminate\Support\Str;
13+
14+
class SendSpeedtestThresholdNotification
15+
{
16+
/**
17+
* Handle the event.
18+
*/
19+
public function handle(SpeedtestCompleted $event): void
20+
{
21+
$notificationSettings = new NotificationSettings;
22+
23+
if (! $notificationSettings->apprise_enabled) {
24+
return;
25+
}
26+
27+
if (! $notificationSettings->apprise_on_threshold_failure) {
28+
return;
29+
}
30+
31+
if (empty($notificationSettings->apprise_channel_urls) || ! is_array($notificationSettings->apprise_channel_urls)) {
32+
Log::warning('Apprise service URLs not found; check Apprise notification settings.');
33+
34+
return;
35+
}
36+
37+
$thresholdSettings = new ThresholdSettings;
38+
39+
if (! $thresholdSettings->absolute_enabled) {
40+
return;
41+
}
42+
43+
$failed = [];
44+
45+
if ($thresholdSettings->absolute_download > 0) {
46+
array_push($failed, $this->absoluteDownloadThreshold(event: $event, thresholdSettings: $thresholdSettings));
47+
}
48+
49+
if ($thresholdSettings->absolute_upload > 0) {
50+
array_push($failed, $this->absoluteUploadThreshold(event: $event, thresholdSettings: $thresholdSettings));
51+
}
52+
53+
if ($thresholdSettings->absolute_ping > 0) {
54+
array_push($failed, $this->absolutePingThreshold(event: $event, thresholdSettings: $thresholdSettings));
55+
}
56+
57+
$failed = array_filter($failed);
58+
59+
if (! count($failed)) {
60+
Log::warning('Failed Apprise thresholds not found, won\'t send notification.');
61+
62+
return;
63+
}
64+
65+
$body = view('apprise.speedtest-threshold', [
66+
'id' => $event->result->id,
67+
'service' => Str::title($event->result->service->getLabel()),
68+
'serverName' => $event->result->server_name,
69+
'serverId' => $event->result->server_id,
70+
'isp' => $event->result->isp,
71+
'metrics' => $failed,
72+
'speedtest_url' => $event->result->result_url,
73+
'url' => url('/admin/results'),
74+
])->render();
75+
76+
$title = 'Speedtest Threshold Breach – #'.$event->result->id;
77+
78+
// Send notification to each configured channel URL
79+
foreach ($notificationSettings->apprise_channel_urls as $row) {
80+
$channelUrl = $row['channel_url'] ?? null;
81+
if (! $channelUrl) {
82+
Log::warning('Skipping entry with missing channel_url.');
83+
84+
continue;
85+
}
86+
87+
Notification::route('apprise_urls', $channelUrl)
88+
->notify(new SpeedtestNotification($title, $body, 'warning'));
89+
}
90+
}
91+
92+
/**
93+
* Build Apprise notification if absolute download threshold is breached.
94+
*/
95+
protected function absoluteDownloadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
96+
{
97+
if (! absoluteDownloadThresholdFailed($thresholdSettings->absolute_download, $event->result->download)) {
98+
return false;
99+
}
100+
101+
return [
102+
'name' => 'Download',
103+
'threshold' => $thresholdSettings->absolute_download.' Mbps',
104+
'value' => Number::toBitRate(bits: $event->result->download_bits, precision: 2),
105+
];
106+
}
107+
108+
/**
109+
* Build Apprise notification if absolute upload threshold is breached.
110+
*/
111+
protected function absoluteUploadThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
112+
{
113+
if (! absoluteUploadThresholdFailed($thresholdSettings->absolute_upload, $event->result->upload)) {
114+
return false;
115+
}
116+
117+
return [
118+
'name' => 'Upload',
119+
'threshold' => $thresholdSettings->absolute_upload.' Mbps',
120+
'value' => Number::toBitRate(bits: $event->result->upload_bits, precision: 2),
121+
];
122+
}
123+
124+
/**
125+
* Build Apprise notification if absolute ping threshold is breached.
126+
*/
127+
protected function absolutePingThreshold(SpeedtestCompleted $event, ThresholdSettings $thresholdSettings): bool|array
128+
{
129+
if (! absolutePingThresholdFailed($thresholdSettings->absolute_ping, $event->result->ping)) {
130+
return false;
131+
}
132+
133+
return [
134+
'name' => 'Ping',
135+
'threshold' => $thresholdSettings->absolute_ping.' ms',
136+
'value' => round($event->result->ping, 2).' ms',
137+
];
138+
}
139+
}

app/Listeners/Database/SendSpeedtestCompletedNotification.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use App\Models\User;
77
use App\Settings\NotificationSettings;
88
use Filament\Notifications\Notification;
9+
use Illuminate\Support\Facades\Log;
910

1011
class SendSpeedtestCompletedNotification
1112
{
@@ -25,6 +26,7 @@ public function handle(SpeedtestCompleted $event): void
2526
}
2627

2728
foreach (User::all() as $user) {
29+
Log::info('Notifying user', ['id' => $user->id, 'email' => $user->email]);
2830
Notification::make()
2931
->title('Speedtest completed')
3032
->success()

app/Listeners/SpeedtestEventSubscriber.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function handleSpeedtestCompleted(SpeedtestCompleted $event): void
2929
{
3030
$settings = app(DataIntegrationSettings::class);
3131

32+
// Write to InfluxDB if enabled
3233
if ($settings->influxdb_v2_enabled) {
3334
WriteResult::dispatch($event->result);
3435
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace App\Notifications\Apprise;
4+
5+
class AppriseMessage
6+
{
7+
public function __construct(
8+
public string|array $urls,
9+
public string $title,
10+
public string $body,
11+
public string $type = 'info',
12+
public string $format = 'text',
13+
public ?string $tag = null,
14+
) {}
15+
16+
public static function create(): self
17+
{
18+
return new self(
19+
urls: '',
20+
title: '',
21+
body: '',
22+
);
23+
}
24+
25+
public function urls(string|array $urls): self
26+
{
27+
$this->urls = $urls;
28+
29+
return $this;
30+
}
31+
32+
public function title(string $title): self
33+
{
34+
$this->title = $title;
35+
36+
return $this;
37+
}
38+
39+
public function body(string $body): self
40+
{
41+
$this->body = $body;
42+
43+
return $this;
44+
}
45+
46+
public function type(string $type): self
47+
{
48+
$this->type = $type;
49+
50+
return $this;
51+
}
52+
53+
public function format(string $format): self
54+
{
55+
$this->format = $format;
56+
57+
return $this;
58+
}
59+
60+
public function tag(string $tag): self
61+
{
62+
$this->tag = $tag;
63+
64+
return $this;
65+
}
66+
}

0 commit comments

Comments
 (0)