From ea49e2da2e9138e353fe6f39bc531f7052586f5f Mon Sep 17 00:00:00 2001
From: = <=>
Date: Fri, 20 Mar 2026 01:34:08 +0100
Subject: [PATCH 1/6] migrated plugin to github from private git server
---
discord-webhooks/LICENSE | 6 +
discord-webhooks/README.md | 47 ++++++
.../migrations/001_create_webhooks_table.php | 29 ++++
discord-webhooks/plugin.json | 17 ++
.../Console/Commands/CheckServerStatus.php | 66 ++++++++
discord-webhooks/src/Enums/WebhookEvent.php | 51 ++++++
.../Webhooks/Pages/ManageWebhooks.php | 11 ++
.../Resources/Webhooks/WebhookResource.php | 149 ++++++++++++++++++
discord-webhooks/src/Models/Webhook.php | 80 ++++++++++
.../src/Policies/WebhookPolicy.php | 8 +
.../src/Providers/WebhooksPluginProvider.php | 84 ++++++++++
.../src/Services/DiscordWebhookService.php | 133 ++++++++++++++++
discord-webhooks/src/WebhooksPlugin.php | 23 +++
13 files changed, 704 insertions(+)
create mode 100644 discord-webhooks/LICENSE
create mode 100644 discord-webhooks/README.md
create mode 100644 discord-webhooks/database/migrations/001_create_webhooks_table.php
create mode 100644 discord-webhooks/plugin.json
create mode 100644 discord-webhooks/src/Console/Commands/CheckServerStatus.php
create mode 100644 discord-webhooks/src/Enums/WebhookEvent.php
create mode 100644 discord-webhooks/src/Filament/Server/Resources/Webhooks/Pages/ManageWebhooks.php
create mode 100644 discord-webhooks/src/Filament/Server/Resources/Webhooks/WebhookResource.php
create mode 100644 discord-webhooks/src/Models/Webhook.php
create mode 100644 discord-webhooks/src/Policies/WebhookPolicy.php
create mode 100644 discord-webhooks/src/Providers/WebhooksPluginProvider.php
create mode 100644 discord-webhooks/src/Services/DiscordWebhookService.php
create mode 100644 discord-webhooks/src/WebhooksPlugin.php
diff --git a/discord-webhooks/LICENSE b/discord-webhooks/LICENSE
new file mode 100644
index 00000000..17b5870a
--- /dev/null
+++ b/discord-webhooks/LICENSE
@@ -0,0 +1,6 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
diff --git a/discord-webhooks/README.md b/discord-webhooks/README.md
new file mode 100644
index 00000000..74edfeb7
--- /dev/null
+++ b/discord-webhooks/README.md
@@ -0,0 +1,47 @@
+# Discord Webhooks (by notjami)
+
+Send Discord webhook notifications for various server events in Pelican Panel.
+
+## Features
+
+- Discord webhook integration
+- Server status notifications (online/offline)
+- Multiple webhooks support
+- Event-based triggers
+- Automatic status checking via scheduled task
+
+## Supported Events
+
+- Server Started (online)
+- Server Stopped (offline)
+- Server Installing
+- Server Installed
+
+## Installation
+
+1. Copy the `discord-webhooks` folder to your Pelican Panel plugins directory or import it with
+2. Install the plugin
+
+## Server Status Detection
+
+Server start/stop detection works via a scheduled command that checks server status every minute.
+
+**Manual check:**
+```bash
+php artisan webhooks:check-status
+```
+
+## Usage
+
+1. Navigate to Admin Panel → Webhooks
+2. Create a new webhook with your Discord webhook URL
+3. Select which events should trigger the webhook
+4. Save and test your webhook
+
+## Configuration
+
+Each webhook can be configured with:
+- **Name**: A friendly name for the webhook
+- **Webhook URL**: Your Discord webhook URL
+- **Events**: Which events trigger this webhook
+- **Enabled**: Toggle the webhook on/off
diff --git a/discord-webhooks/database/migrations/001_create_webhooks_table.php b/discord-webhooks/database/migrations/001_create_webhooks_table.php
new file mode 100644
index 00000000..20497f6c
--- /dev/null
+++ b/discord-webhooks/database/migrations/001_create_webhooks_table.php
@@ -0,0 +1,29 @@
+increments('id');
+ $table->string('name');
+ $table->string('webhook_url');
+ $table->unsignedInteger('server_id')->nullable();
+ $table->json('events')->default('[]');
+ $table->boolean('enabled')->default(true);
+ $table->timestamp('last_triggered_at')->nullable();
+ $table->timestamps();
+
+ $table->foreign('server_id')->references('id')->on('servers')->nullOnDelete();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('webhooks');
+ }
+};
diff --git a/discord-webhooks/plugin.json b/discord-webhooks/plugin.json
new file mode 100644
index 00000000..8909db0c
--- /dev/null
+++ b/discord-webhooks/plugin.json
@@ -0,0 +1,17 @@
+{
+ "id": "discord-webhooks",
+ "name": "Discord Webhooks",
+ "author": "notjami",
+ "version": "1.1.1",
+ "description": "Send Discord webhook notifications for server events",
+ "category": "plugin",
+ "url": "https://github.com/jami100YT/pelican-plugins/tree/discord-webhooks/discord-webhooks",
+ "update_url": null,
+ "namespace": "Notjami\\Webhooks",
+ "class": "WebhooksPlugin",
+ "panels": [
+ "server"
+ ],
+ "panel_version": null,
+ "composer_packages": null
+}
diff --git a/discord-webhooks/src/Console/Commands/CheckServerStatus.php b/discord-webhooks/src/Console/Commands/CheckServerStatus.php
new file mode 100644
index 00000000..6d035089
--- /dev/null
+++ b/discord-webhooks/src/Console/Commands/CheckServerStatus.php
@@ -0,0 +1,66 @@
+whereNotNull('server_id')
+ ->pluck('server_id')
+ ->unique();
+
+ // Also check servers if there are global webhooks
+ $hasGlobalWebhooks = Webhook::enabled()
+ ->whereNull('server_id')
+ ->exists();
+
+ if ($hasGlobalWebhooks) {
+ $servers = Server::whereNull('status')->get();
+ } else {
+ $servers = Server::whereIn('id', $serverIds)->whereNull('status')->get();
+ }
+
+ foreach ($servers as $server) {
+ try {
+ $details = $repository->setServer($server)->getDetails();
+ $currentState = $details['state'] ?? 'offline';
+
+ $cacheKey = "webhook_server_status_{$server->id}";
+ $previousState = Cache::get($cacheKey, 'unknown');
+
+ if ($previousState !== $currentState) {
+ Cache::put($cacheKey, $currentState, now()->addHours(24));
+
+ if ($previousState !== 'unknown') {
+ if ($currentState === 'running') {
+ $webhookService->triggerEvent(WebhookEvent::ServerStarted, $server);
+ $this->info("Server {$server->name} started - webhook triggered");
+ } elseif (in_array($currentState, ['offline', 'stopped'])) {
+ $webhookService->triggerEvent(WebhookEvent::ServerStopped, $server);
+ $this->info("Server {$server->name} stopped - webhook triggered");
+ }
+ }
+ }
+ } catch (\Exception $e) {
+ $this->error("Failed to check server {$server->name}: {$e->getMessage()}");
+ }
+ }
+
+ return self::SUCCESS;
+ }
+}
diff --git a/discord-webhooks/src/Enums/WebhookEvent.php b/discord-webhooks/src/Enums/WebhookEvent.php
new file mode 100644
index 00000000..202ed655
--- /dev/null
+++ b/discord-webhooks/src/Enums/WebhookEvent.php
@@ -0,0 +1,51 @@
+ 'Server Started',
+ self::ServerStopped => 'Server Stopped',
+ self::ServerInstalling => 'Server Installing',
+ self::ServerInstalled => 'Server Installed',
+ };
+ }
+
+ public function getDescription(): string
+ {
+ return match ($this) {
+ self::ServerStarted => 'Triggered when a server comes online',
+ self::ServerStopped => 'Triggered when a server goes offline',
+ self::ServerInstalling => 'Triggered when a server starts installing',
+ self::ServerInstalled => 'Triggered when a server finishes installation',
+ };
+ }
+
+ public function getColor(): string
+ {
+ return match ($this) {
+ self::ServerStarted => '3066993', // Green
+ self::ServerStopped => '15158332', // Red
+ self::ServerInstalling => '15105570', // Orange
+ self::ServerInstalled => '3447003', // Blue
+ };
+ }
+
+ public function getEmoji(): string
+ {
+ return match ($this) {
+ self::ServerStarted => '🟢',
+ self::ServerStopped => '🔴',
+ self::ServerInstalling => '🔧',
+ self::ServerInstalled => '✅',
+ };
+ }
+}
diff --git a/discord-webhooks/src/Filament/Server/Resources/Webhooks/Pages/ManageWebhooks.php b/discord-webhooks/src/Filament/Server/Resources/Webhooks/Pages/ManageWebhooks.php
new file mode 100644
index 00000000..c5e9b226
--- /dev/null
+++ b/discord-webhooks/src/Filament/Server/Resources/Webhooks/Pages/ManageWebhooks.php
@@ -0,0 +1,11 @@
+count() ?: null;
+ }
+
+ public static function table(Table $table): Table
+ {
+ return $table
+ ->columns([
+ TextColumn::make('name')
+ ->label('Name')
+ ->searchable()
+ ->sortable(),
+ TextColumn::make('webhook_url')
+ ->label('Webhook URL')
+ ->limit(30)
+ ->tooltip(fn ($state) => $state),
+ TextColumn::make('events')
+ ->label('Events')
+ ->badge()
+ ->formatStateUsing(function ($state) {
+ return collect($state)->map(fn ($event) => WebhookEvent::from($event)->getLabel())->join(', ');
+ }),
+ IconColumn::make('enabled')
+ ->label('Enabled')
+ ->boolean(),
+ TextColumn::make('last_triggered_at')
+ ->label('Last Triggered')
+ ->dateTime()
+ ->placeholder('Never'),
+ ])
+ ->recordActions([
+ Action::make('test')
+ ->label('Test')
+ ->icon('tabler-send')
+ ->color('info')
+ ->requiresConfirmation()
+ ->modalHeading('Test Webhook')
+ ->modalDescription('This will send a test message to the webhook URL.')
+ ->action(function (Webhook $record, DiscordWebhookService $service) {
+ $success = $service->sendTestMessage($record);
+
+ if ($success) {
+ Notification::make()
+ ->title('Test message sent!')
+ ->success()
+ ->send();
+ } else {
+ Notification::make()
+ ->title('Failed to send test message')
+ ->danger()
+ ->send();
+ }
+ }),
+ EditAction::make(),
+ DeleteAction::make(),
+ ])
+ ->toolbarActions([
+ CreateAction::make()
+ ->createAnother(false),
+ ])
+ ->emptyStateIcon('tabler-webhook')
+ ->emptyStateDescription('')
+ ->emptyStateHeading('No webhooks configured');
+ }
+
+ public static function form(Schema $schema): Schema
+ {
+ return $schema
+ ->components([
+ TextInput::make('name')
+ ->label('Name')
+ ->placeholder('My Discord Webhook')
+ ->required()
+ ->maxLength(255)
+ ->columnSpanFull(),
+ TextInput::make('webhook_url')
+ ->label('Discord Webhook URL')
+ ->placeholder('https://discord.com/api/webhooks/...')
+ ->required()
+ ->url()
+ ->maxLength(500)
+ ->columnSpanFull(),
+ Select::make('events')
+ ->label('Events')
+ ->multiple()
+ ->options(collect(WebhookEvent::cases())->mapWithKeys(fn ($event) => [
+ $event->value => $event->getLabel(),
+ ]))
+ ->required()
+ ->columnSpanFull(),
+ Toggle::make('enabled')
+ ->label('Enabled')
+ ->default(true)
+ ->inline(false),
+ ]);
+ }
+
+ public static function getPages(): array
+ {
+ return [
+ 'index' => ManageWebhooks::route('/'),
+ ];
+ }
+}
diff --git a/discord-webhooks/src/Models/Webhook.php b/discord-webhooks/src/Models/Webhook.php
new file mode 100644
index 00000000..637eef1b
--- /dev/null
+++ b/discord-webhooks/src/Models/Webhook.php
@@ -0,0 +1,80 @@
+ $events
+ * @property bool $enabled
+ * @property Carbon|null $last_triggered_at
+ * @property Carbon $created_at
+ * @property Carbon $updated_at
+ * @property Server|null $server
+ */
+class Webhook extends Model
+{
+ protected $fillable = [
+ 'name',
+ 'webhook_url',
+ 'server_id',
+ 'events',
+ 'enabled',
+ 'last_triggered_at',
+ ];
+
+ protected $attributes = [
+ 'events' => '[]',
+ 'enabled' => true,
+ ];
+
+ protected function casts(): array
+ {
+ return [
+ 'events' => 'array',
+ 'enabled' => 'boolean',
+ 'last_triggered_at' => 'datetime',
+ ];
+ }
+
+ public function server(): BelongsTo
+ {
+ return $this->belongsTo(Server::class);
+ }
+
+ public function hasEvent(WebhookEvent $event): bool
+ {
+ return in_array($event->value, $this->events);
+ }
+
+ public function appliesToServer(Server $server): bool
+ {
+ return $this->server_id === null || $this->server_id === $server->id;
+ }
+
+ public function scopeEnabled($query)
+ {
+ return $query->where('enabled', true);
+ }
+
+ public function scopeForEvent($query, WebhookEvent $event)
+ {
+ return $query->whereJsonContains('events', $event->value);
+ }
+
+ public function scopeForServer($query, Server $server)
+ {
+ return $query->where(function ($q) use ($server) {
+ $q->whereNull('server_id')
+ ->orWhere('server_id', $server->id);
+ });
+ }
+}
diff --git a/discord-webhooks/src/Policies/WebhookPolicy.php b/discord-webhooks/src/Policies/WebhookPolicy.php
new file mode 100644
index 00000000..7ef20ba2
--- /dev/null
+++ b/discord-webhooks/src/Policies/WebhookPolicy.php
@@ -0,0 +1,8 @@
+app->singleton(DiscordWebhookService::class, function () {
+ return new DiscordWebhookService();
+ });
+
+ // Register console commands
+ $this->commands([
+ CheckServerStatus::class,
+ ]);
+ }
+
+ public function boot(): void
+ {
+ $this->registerServerStatusListeners();
+ $this->registerScheduledTasks();
+ }
+
+ protected function registerServerStatusListeners(): void
+ {
+ // Listen for Pelican Panel server events
+ // These events are dispatched when server status changes
+
+ // Server Installation Events
+ Event::listen('eloquent.updating: App\Models\Server', function (Server $server) {
+ $service = app(DiscordWebhookService::class);
+
+ // Check if status changed to installing
+ if ($server->isDirty('status') && $server->status === 'installing') {
+ $service->triggerEvent(WebhookEvent::ServerInstalling, $server);
+ }
+
+ // Check if installation completed (status changed from installing to null/running)
+ if ($server->isDirty('status') && $server->getOriginal('status') === 'installing' && $server->status === null) {
+ $service->triggerEvent(WebhookEvent::ServerInstalled, $server);
+ }
+ });
+
+ // Try to listen for daemon status events if they exist
+ $statusEvents = [
+ 'App\Events\Server\Started' => WebhookEvent::ServerStarted,
+ 'App\Events\Server\Stopped' => WebhookEvent::ServerStopped,
+ 'App\Events\Server\Starting' => WebhookEvent::ServerStarted,
+ 'App\Events\Server\Stopping' => WebhookEvent::ServerStopped,
+ ];
+
+ foreach ($statusEvents as $eventClass => $webhookEvent) {
+ if (class_exists($eventClass)) {
+ Event::listen($eventClass, function ($event) use ($webhookEvent) {
+ $server = $event->server ?? null;
+ if ($server instanceof Server) {
+ app(DiscordWebhookService::class)->triggerEvent($webhookEvent, $server);
+ }
+ });
+ }
+ }
+ }
+
+ protected function registerScheduledTasks(): void
+ {
+ $this->app->booted(function () {
+ $schedule = $this->app->make(Schedule::class);
+ $schedule->command('discord-webhooks:check-status')->everyMinute();
+ });
+ }
+}
diff --git a/discord-webhooks/src/Services/DiscordWebhookService.php b/discord-webhooks/src/Services/DiscordWebhookService.php
new file mode 100644
index 00000000..ef85b43a
--- /dev/null
+++ b/discord-webhooks/src/Services/DiscordWebhookService.php
@@ -0,0 +1,133 @@
+ [
+ [
+ 'title' => '🔔 Webhook Test',
+ 'description' => 'This is a test message from Pelican Panel.',
+ 'color' => 3447003,
+ 'fields' => [
+ [
+ 'name' => 'Webhook Name',
+ 'value' => $webhook->name,
+ 'inline' => true,
+ ],
+ [
+ 'name' => 'Status',
+ 'value' => '✅ Working',
+ 'inline' => true,
+ ],
+ ],
+ 'footer' => [
+ 'text' => 'Pelican Panel Webhooks',
+ ],
+ 'timestamp' => now()->toIso8601String(),
+ ],
+ ],
+ ];
+
+ return $this->send($webhook, $payload);
+ }
+
+ public function sendServerEvent(Webhook $webhook, Server $server, WebhookEvent $event): bool
+ {
+ $payload = [
+ 'embeds' => [
+ [
+ 'title' => $event->getEmoji() . ' ' . $event->getLabel(),
+ 'description' => $this->getEventDescription($event, $server),
+ 'color' => (int) $event->getColor(),
+ 'fields' => [
+ [
+ 'name' => 'Server',
+ 'value' => $server->name,
+ 'inline' => true,
+ ],
+ [
+ 'name' => 'UUID',
+ 'value' => '`' . substr($server->uuid, 0, 8) . '...`',
+ 'inline' => true,
+ ],
+ [
+ 'name' => 'Owner',
+ 'value' => $server->user->username ?? 'Unknown',
+ 'inline' => true,
+ ],
+ [
+ 'name' => 'Node',
+ 'value' => $server->node->name ?? 'Unknown',
+ 'inline' => true,
+ ],
+ ],
+ 'footer' => [
+ 'text' => 'Pelican Panel Webhooks',
+ ],
+ 'timestamp' => now()->toIso8601String(),
+ ],
+ ],
+ ];
+
+ return $this->send($webhook, $payload);
+ }
+
+ public function triggerEvent(WebhookEvent $event, Server $server): void
+ {
+ $webhooks = Webhook::enabled()
+ ->forEvent($event)
+ ->forServer($server)
+ ->get();
+
+ foreach ($webhooks as $webhook) {
+ try {
+ $this->sendServerEvent($webhook, $server, $event);
+
+ $webhook->update(['last_triggered_at' => now()]);
+ } catch (\Exception $e) {
+ Log::error('Failed to send webhook', [
+ 'webhook_id' => $webhook->id,
+ 'event' => $event->value,
+ 'error' => $e->getMessage(),
+ ]);
+ }
+ }
+ }
+
+ protected function send(Webhook $webhook, array $payload): bool
+ {
+ try {
+ $response = Http::timeout(10)
+ ->post($webhook->webhook_url, $payload);
+
+ return $response->successful();
+ } catch (\Exception $e) {
+ Log::error('Discord webhook failed', [
+ 'webhook_id' => $webhook->id,
+ 'error' => $e->getMessage(),
+ ]);
+
+ return false;
+ }
+ }
+
+ protected function getEventDescription(WebhookEvent $event, Server $server): string
+ {
+ return match ($event) {
+ WebhookEvent::ServerStarted => "Server **{$server->name}** is now online.",
+ WebhookEvent::ServerStopped => "Server **{$server->name}** has gone offline.",
+ WebhookEvent::ServerInstalling => "Server **{$server->name}** is being installed.",
+ WebhookEvent::ServerInstalled => "Server **{$server->name}** has been installed successfully.",
+ };
+ }
+}
diff --git a/discord-webhooks/src/WebhooksPlugin.php b/discord-webhooks/src/WebhooksPlugin.php
new file mode 100644
index 00000000..33624a90
--- /dev/null
+++ b/discord-webhooks/src/WebhooksPlugin.php
@@ -0,0 +1,23 @@
+getId())->title();
+
+ $panel->discoverResources(plugin_path($this->getId(), "src/Filament/$id/Resources"), "Notjami\\Webhooks\\Filament\\$id\\Resources");
+ }
+
+ public function boot(Panel $panel): void {}
+}
From 607eb9c4388023b276ad146e937f33f7831756bb Mon Sep 17 00:00:00 2001
From: = <=>
Date: Fri, 20 Mar 2026 01:37:32 +0100
Subject: [PATCH 2/6] update installation instructions with correct URL for
plugin import
---
discord-webhooks/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/discord-webhooks/README.md b/discord-webhooks/README.md
index 74edfeb7..d34241d3 100644
--- a/discord-webhooks/README.md
+++ b/discord-webhooks/README.md
@@ -19,7 +19,7 @@ Send Discord webhook notifications for various server events in Pelican Panel.
## Installation
-1. Copy the `discord-webhooks` folder to your Pelican Panel plugins directory or import it with
+1. Copy the `discord-webhooks` folder to your Pelican Panel plugins directory or import it with `https://github.com/jami100YT/pelican-plugins/archive/refs/tags/latest.zip`
2. Install the plugin
## Server Status Detection
From 7c93432ea82e97c048668382a583b8e2ba4f500d Mon Sep 17 00:00:00 2001
From: = <=>
Date: Fri, 20 Mar 2026 02:36:36 +0100
Subject: [PATCH 3/6] refactor: improve webhook handling and validation, update
installation instructions
---
discord-webhooks/README.md | 4 +--
.../migrations/001_create_webhooks_table.php | 4 +--
.../Console/Commands/CheckServerStatus.php | 4 +--
.../Webhooks/Pages/ManageWebhooks.php | 2 +-
.../Resources/Webhooks/WebhookResource.php | 34 ++++++++++++++-----
discord-webhooks/src/Models/Webhook.php | 21 ++++++++----
.../src/Providers/WebhooksPluginProvider.php | 17 ++++++----
.../src/Services/DiscordWebhookService.php | 15 ++++++--
8 files changed, 69 insertions(+), 32 deletions(-)
diff --git a/discord-webhooks/README.md b/discord-webhooks/README.md
index d34241d3..b7cb272f 100644
--- a/discord-webhooks/README.md
+++ b/discord-webhooks/README.md
@@ -19,7 +19,7 @@ Send Discord webhook notifications for various server events in Pelican Panel.
## Installation
-1. Copy the `discord-webhooks` folder to your Pelican Panel plugins directory or import it with `https://github.com/jami100YT/pelican-plugins/archive/refs/tags/latest.zip`
+1. Copy the `discord-webhooks` folder to your Pelican Panel plugins directory
2. Install the plugin
## Server Status Detection
@@ -28,7 +28,7 @@ Server start/stop detection works via a scheduled command that checks server sta
**Manual check:**
```bash
-php artisan webhooks:check-status
+php artisan discord-webhooks:check-status
```
## Usage
diff --git a/discord-webhooks/database/migrations/001_create_webhooks_table.php b/discord-webhooks/database/migrations/001_create_webhooks_table.php
index 20497f6c..539a4d54 100644
--- a/discord-webhooks/database/migrations/001_create_webhooks_table.php
+++ b/discord-webhooks/database/migrations/001_create_webhooks_table.php
@@ -12,13 +12,13 @@ public function up(): void
$table->increments('id');
$table->string('name');
$table->string('webhook_url');
- $table->unsignedInteger('server_id')->nullable();
+ $table->foreignId('server_id')->nullable()->constrained('servers')->nullOnDelete();
$table->json('events')->default('[]');
$table->boolean('enabled')->default(true);
$table->timestamp('last_triggered_at')->nullable();
$table->timestamps();
- $table->foreign('server_id')->references('id')->on('servers')->nullOnDelete();
+ // foreignId above handles FK constraint and nullOnDelete
});
}
diff --git a/discord-webhooks/src/Console/Commands/CheckServerStatus.php b/discord-webhooks/src/Console/Commands/CheckServerStatus.php
index 6d035089..da5c10be 100644
--- a/discord-webhooks/src/Console/Commands/CheckServerStatus.php
+++ b/discord-webhooks/src/Console/Commands/CheckServerStatus.php
@@ -42,10 +42,10 @@ public function handle(DaemonServerRepository $repository, DiscordWebhookService
$cacheKey = "webhook_server_status_{$server->id}";
$previousState = Cache::get($cacheKey, 'unknown');
+ // Always refresh the cache TTL, even if state hasn't changed
+ Cache::put($cacheKey, $currentState, now()->addHours(24));
if ($previousState !== $currentState) {
- Cache::put($cacheKey, $currentState, now()->addHours(24));
-
if ($previousState !== 'unknown') {
if ($currentState === 'running') {
$webhookService->triggerEvent(WebhookEvent::ServerStarted, $server);
diff --git a/discord-webhooks/src/Filament/Server/Resources/Webhooks/Pages/ManageWebhooks.php b/discord-webhooks/src/Filament/Server/Resources/Webhooks/Pages/ManageWebhooks.php
index c5e9b226..09701270 100644
--- a/discord-webhooks/src/Filament/Server/Resources/Webhooks/Pages/ManageWebhooks.php
+++ b/discord-webhooks/src/Filament/Server/Resources/Webhooks/Pages/ManageWebhooks.php
@@ -2,8 +2,8 @@
namespace Notjami\Webhooks\Filament\Server\Resources\Webhooks\Pages;
-use Notjami\Webhooks\Filament\Server\Resources\Webhooks\WebhookResource;
use Filament\Resources\Pages\ManageRecords;
+use Notjami\Webhooks\Filament\Server\Resources\Webhooks\WebhookResource;
class ManageWebhooks extends ManageRecords
{
diff --git a/discord-webhooks/src/Filament/Server/Resources/Webhooks/WebhookResource.php b/discord-webhooks/src/Filament/Server/Resources/Webhooks/WebhookResource.php
index bd3f5a00..3aed88da 100644
--- a/discord-webhooks/src/Filament/Server/Resources/Webhooks/WebhookResource.php
+++ b/discord-webhooks/src/Filament/Server/Resources/Webhooks/WebhookResource.php
@@ -2,15 +2,10 @@
namespace Notjami\Webhooks\Filament\Server\Resources\Webhooks;
-use App\Models\Server;
-use Notjami\Webhooks\Filament\Server\Resources\Webhooks\Pages\ManageWebhooks;
-use Notjami\Webhooks\Models\Webhook;
-use Notjami\Webhooks\Enums\WebhookEvent;
-use Notjami\Webhooks\Services\DiscordWebhookService;
+use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
-use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
@@ -20,7 +15,10 @@
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
-
+use Notjami\Webhooks\Enums\WebhookEvent;
+use Notjami\Webhooks\Filament\Server\Resources\Webhooks\Pages\ManageWebhooks;
+use Notjami\Webhooks\Models\Webhook;
+use Notjami\Webhooks\Services\DiscordWebhookService;
class WebhookResource extends Resource
{
protected static ?string $model = Webhook::class;
@@ -58,12 +56,29 @@ public static function table(Table $table): Table
TextColumn::make('webhook_url')
->label('Webhook URL')
->limit(30)
- ->tooltip(fn ($state) => $state),
+ ->formatStateUsing(function ($state) {
+ // Mask the URL: show scheme://host/...****
+ if (empty($state)) return '';
+ $parsed = parse_url($state);
+ if (!$parsed || !isset($parsed['scheme'], $parsed['host'])) return '••••••';
+ $last4 = substr($parsed['path'] ?? '', -4);
+ $masked = $parsed['scheme'] . '://' . $parsed['host'] . '/...';
+ if ($last4) {
+ $masked .= $last4;
+ }
+ return $masked;
+ })
+ ->tooltip('••••••'),
TextColumn::make('events')
->label('Events')
->badge()
->formatStateUsing(function ($state) {
- return collect($state)->map(fn ($event) => WebhookEvent::from($event)->getLabel())->join(', ');
+ $events = is_array($state) ? $state : (array) $state;
+ return collect($events)
+ ->map(fn ($event) => WebhookEvent::tryFrom($event))
+ ->filter()
+ ->map(fn ($event) => $event->getLabel())
+ ->join(', ');
}),
IconColumn::make('enabled')
->label('Enabled')
@@ -123,6 +138,7 @@ public static function form(Schema $schema): Schema
->placeholder('https://discord.com/api/webhooks/...')
->required()
->url()
+ ->regex('/^https:\/\/discord\\.com\/api\/webhooks\/.+/')
->maxLength(500)
->columnSpanFull(),
Select::make('events')
diff --git a/discord-webhooks/src/Models/Webhook.php b/discord-webhooks/src/Models/Webhook.php
index 637eef1b..bcc3b745 100644
--- a/discord-webhooks/src/Models/Webhook.php
+++ b/discord-webhooks/src/Models/Webhook.php
@@ -2,6 +2,8 @@
namespace Notjami\Webhooks\Models;
+use Illuminate\Database\Eloquent\Builder;
+
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
@@ -50,9 +52,16 @@ public function server(): BelongsTo
return $this->belongsTo(Server::class);
}
- public function hasEvent(WebhookEvent $event): bool
+ /**
+ * Check if the webhook has the given event.
+ *
+ * @param WebhookEvent|string $event
+ * @return bool
+ */
+ public function hasEvent(WebhookEvent|string $event): bool
{
- return in_array($event->value, $this->events);
+ $value = $event instanceof WebhookEvent ? $event->value : $event;
+ return in_array($value, $this->events, true);
}
public function appliesToServer(Server $server): bool
@@ -60,21 +69,21 @@ public function appliesToServer(Server $server): bool
return $this->server_id === null || $this->server_id === $server->id;
}
- public function scopeEnabled($query)
+ public function scopeEnabled(Builder $query): Builder
{
return $query->where('enabled', true);
}
- public function scopeForEvent($query, WebhookEvent $event)
+ public function scopeForEvent(Builder $query, WebhookEvent $event): Builder
{
return $query->whereJsonContains('events', $event->value);
}
- public function scopeForServer($query, Server $server)
+ public function scopeForServer(Builder $query, Server $server): Builder
{
return $query->where(function ($q) use ($server) {
$q->whereNull('server_id')
- ->orWhere('server_id', $server->id);
+ ->orWhere('server_id', $server->id);
});
}
}
diff --git a/discord-webhooks/src/Providers/WebhooksPluginProvider.php b/discord-webhooks/src/Providers/WebhooksPluginProvider.php
index 7c1e32de..82afe4ab 100644
--- a/discord-webhooks/src/Providers/WebhooksPluginProvider.php
+++ b/discord-webhooks/src/Providers/WebhooksPluginProvider.php
@@ -41,16 +41,18 @@ protected function registerServerStatusListeners(): void
// Server Installation Events
Event::listen('eloquent.updating: App\Models\Server', function (Server $server) {
- $service = app(DiscordWebhookService::class);
-
// Check if status changed to installing
if ($server->isDirty('status') && $server->status === 'installing') {
- $service->triggerEvent(WebhookEvent::ServerInstalling, $server);
+ DB::afterCommit(function () use ($server) {
+ app(DiscordWebhookService::class)->triggerEvent(WebhookEvent::ServerInstalling, $server);
+ });
}
// Check if installation completed (status changed from installing to null/running)
if ($server->isDirty('status') && $server->getOriginal('status') === 'installing' && $server->status === null) {
- $service->triggerEvent(WebhookEvent::ServerInstalled, $server);
+ DB::afterCommit(function () use ($server) {
+ app(DiscordWebhookService::class)->triggerEvent(WebhookEvent::ServerInstalled, $server);
+ });
}
});
@@ -58,8 +60,6 @@ protected function registerServerStatusListeners(): void
$statusEvents = [
'App\Events\Server\Started' => WebhookEvent::ServerStarted,
'App\Events\Server\Stopped' => WebhookEvent::ServerStopped,
- 'App\Events\Server\Starting' => WebhookEvent::ServerStarted,
- 'App\Events\Server\Stopping' => WebhookEvent::ServerStopped,
];
foreach ($statusEvents as $eventClass => $webhookEvent) {
@@ -67,7 +67,10 @@ protected function registerServerStatusListeners(): void
Event::listen($eventClass, function ($event) use ($webhookEvent) {
$server = $event->server ?? null;
if ($server instanceof Server) {
- app(DiscordWebhookService::class)->triggerEvent($webhookEvent, $server);
+ // Only update the cache/state, do not send webhook here
+ $cacheKey = "webhook_server_status_{$server->id}";
+ $state = $webhookEvent === WebhookEvent::ServerStarted ? 'running' : 'offline';
+ \Illuminate\Support\Facades\Cache::put($cacheKey, $state, now()->addHours(24));
}
});
}
diff --git a/discord-webhooks/src/Services/DiscordWebhookService.php b/discord-webhooks/src/Services/DiscordWebhookService.php
index ef85b43a..c13f000b 100644
--- a/discord-webhooks/src/Services/DiscordWebhookService.php
+++ b/discord-webhooks/src/Services/DiscordWebhookService.php
@@ -91,9 +91,10 @@ public function triggerEvent(WebhookEvent $event, Server $server): void
foreach ($webhooks as $webhook) {
try {
- $this->sendServerEvent($webhook, $server, $event);
-
- $webhook->update(['last_triggered_at' => now()]);
+ $sent = $this->sendServerEvent($webhook, $server, $event);
+ if ($sent) {
+ $webhook->update(['last_triggered_at' => now()]);
+ }
} catch (\Exception $e) {
Log::error('Failed to send webhook', [
'webhook_id' => $webhook->id,
@@ -106,6 +107,14 @@ public function triggerEvent(WebhookEvent $event, Server $server): void
protected function send(Webhook $webhook, array $payload): bool
{
+ // Enforce Discord webhook URL pattern as a second layer of validation
+ if (!preg_match('/^https:\/\/discord\.com\/api\/webhooks\/.+/', $webhook->webhook_url)) {
+ Log::warning('Rejected non-Discord webhook URL', [
+ 'webhook_id' => $webhook->id,
+ 'url' => $webhook->webhook_url,
+ ]);
+ return false;
+ }
try {
$response = Http::timeout(10)
->post($webhook->webhook_url, $payload);
From 1e0a005f869dff873bcc95bd6a07e8493ae9bf53 Mon Sep 17 00:00:00 2001
From: = <=>
Date: Fri, 20 Mar 2026 02:49:27 +0100
Subject: [PATCH 4/6] refactor: update property type for events in Webhook
model and enhance server status checks in event listener
---
discord-webhooks/src/Models/Webhook.php | 5 +++--
.../src/Providers/WebhooksPluginProvider.php | 11 +++++++++--
.../src/Services/DiscordWebhookService.php | 3 +++
3 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/discord-webhooks/src/Models/Webhook.php b/discord-webhooks/src/Models/Webhook.php
index bcc3b745..409514fc 100644
--- a/discord-webhooks/src/Models/Webhook.php
+++ b/discord-webhooks/src/Models/Webhook.php
@@ -15,7 +15,7 @@
* @property string $name
* @property string $webhook_url
* @property int|null $server_id
- * @property array $events
+ * @property array $events
* @property bool $enabled
* @property Carbon|null $last_triggered_at
* @property Carbon $created_at
@@ -60,7 +60,8 @@ public function server(): BelongsTo
*/
public function hasEvent(WebhookEvent|string $event): bool
{
- $value = $event instanceof WebhookEvent ? $event->value : $event;
+ $value = $event instanceof WebhookEvent ? $event->value : (string)$event;
+ // $this->events ist array
return in_array($value, $this->events, true);
}
diff --git a/discord-webhooks/src/Providers/WebhooksPluginProvider.php b/discord-webhooks/src/Providers/WebhooksPluginProvider.php
index 82afe4ab..7b173d3d 100644
--- a/discord-webhooks/src/Providers/WebhooksPluginProvider.php
+++ b/discord-webhooks/src/Providers/WebhooksPluginProvider.php
@@ -2,6 +2,7 @@
namespace Notjami\Webhooks\Providers;
+use Illuminate\Support\Facades\DB;
use App\Models\Role;
use App\Models\Server;
use Illuminate\Console\Scheduling\Schedule;
@@ -42,14 +43,20 @@ protected function registerServerStatusListeners(): void
// Server Installation Events
Event::listen('eloquent.updating: App\Models\Server', function (Server $server) {
// Check if status changed to installing
- if ($server->isDirty('status') && $server->status === 'installing') {
+ if (
+ $server->isDirty('status') &&
+ ((is_string($server->status) && $server->status === 'installing') ||
+ (method_exists($server->status, 'value') && $server->status->value === 'installing'))
+ ) {
DB::afterCommit(function () use ($server) {
app(DiscordWebhookService::class)->triggerEvent(WebhookEvent::ServerInstalling, $server);
});
}
// Check if installation completed (status changed from installing to null/running)
- if ($server->isDirty('status') && $server->getOriginal('status') === 'installing' && $server->status === null) {
+ $original = $server->getOriginal('status');
+ $isOriginalInstalling = (is_string($original) && $original === 'installing') || (method_exists($original, 'value') && $original->value === 'installing');
+ if ($server->isDirty('status') && $isOriginalInstalling && $server->status === null) {
DB::afterCommit(function () use ($server) {
app(DiscordWebhookService::class)->triggerEvent(WebhookEvent::ServerInstalled, $server);
});
diff --git a/discord-webhooks/src/Services/DiscordWebhookService.php b/discord-webhooks/src/Services/DiscordWebhookService.php
index c13f000b..f85c9524 100644
--- a/discord-webhooks/src/Services/DiscordWebhookService.php
+++ b/discord-webhooks/src/Services/DiscordWebhookService.php
@@ -105,6 +105,9 @@ public function triggerEvent(WebhookEvent $event, Server $server): void
}
}
+ /**
+ * @param array $payload
+ */
protected function send(Webhook $webhook, array $payload): bool
{
// Enforce Discord webhook URL pattern as a second layer of validation
From 239082c91884105dc98291e25286a236505d32cb Mon Sep 17 00:00:00 2001
From: = <=>
Date: Fri, 20 Mar 2026 02:55:27 +0100
Subject: [PATCH 5/6] refactor: enhance server status checks in event listener
for installation events
---
.../src/Providers/WebhooksPluginProvider.php | 20 +++++++++++--------
1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/discord-webhooks/src/Providers/WebhooksPluginProvider.php b/discord-webhooks/src/Providers/WebhooksPluginProvider.php
index 7b173d3d..244f5d14 100644
--- a/discord-webhooks/src/Providers/WebhooksPluginProvider.php
+++ b/discord-webhooks/src/Providers/WebhooksPluginProvider.php
@@ -43,20 +43,24 @@ protected function registerServerStatusListeners(): void
// Server Installation Events
Event::listen('eloquent.updating: App\Models\Server', function (Server $server) {
// Check if status changed to installing
- if (
- $server->isDirty('status') &&
- ((is_string($server->status) && $server->status === 'installing') ||
- (method_exists($server->status, 'value') && $server->status->value === 'installing'))
- ) {
+ $status = $server->status;
+ $isInstalling = (
+ (is_string($status) && $status === 'installing') ||
+ ($status instanceof \BackedEnum && $status->value === 'installing')
+ );
+ if ($server->isDirty('status') && $isInstalling) {
DB::afterCommit(function () use ($server) {
app(DiscordWebhookService::class)->triggerEvent(WebhookEvent::ServerInstalling, $server);
});
}
- // Check if installation completed (status changed from installing to null/running)
+ // Check if installation completed (status changed from installing to null)
$original = $server->getOriginal('status');
- $isOriginalInstalling = (is_string($original) && $original === 'installing') || (method_exists($original, 'value') && $original->value === 'installing');
- if ($server->isDirty('status') && $isOriginalInstalling && $server->status === null) {
+ $wasInstalling = (
+ (is_string($original) && $original === 'installing') ||
+ ($original instanceof \BackedEnum && $original->value === 'installing')
+ );
+ if ($server->isDirty('status') && $wasInstalling && $server->status === null) {
DB::afterCommit(function () use ($server) {
app(DiscordWebhookService::class)->triggerEvent(WebhookEvent::ServerInstalled, $server);
});
From 14824993e1443f834fcdcc2d685d26763ff57712 Mon Sep 17 00:00:00 2001
From: = <=>
Date: Fri, 20 Mar 2026 03:15:07 +0100
Subject: [PATCH 6/6] refactor: improve server status handling and clean up
cache updates in event listeners
---
.../src/Providers/WebhooksPluginProvider.php | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/discord-webhooks/src/Providers/WebhooksPluginProvider.php b/discord-webhooks/src/Providers/WebhooksPluginProvider.php
index 244f5d14..27b7dcb1 100644
--- a/discord-webhooks/src/Providers/WebhooksPluginProvider.php
+++ b/discord-webhooks/src/Providers/WebhooksPluginProvider.php
@@ -3,6 +3,8 @@
namespace Notjami\Webhooks\Providers;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Cache;
+use BackedEnum;
use App\Models\Role;
use App\Models\Server;
use Illuminate\Console\Scheduling\Schedule;
@@ -46,7 +48,7 @@ protected function registerServerStatusListeners(): void
$status = $server->status;
$isInstalling = (
(is_string($status) && $status === 'installing') ||
- ($status instanceof \BackedEnum && $status->value === 'installing')
+ ($status instanceof BackedEnum && $status->value === 'installing')
);
if ($server->isDirty('status') && $isInstalling) {
DB::afterCommit(function () use ($server) {
@@ -58,7 +60,7 @@ protected function registerServerStatusListeners(): void
$original = $server->getOriginal('status');
$wasInstalling = (
(is_string($original) && $original === 'installing') ||
- ($original instanceof \BackedEnum && $original->value === 'installing')
+ ($original instanceof BackedEnum && $original->value === 'installing')
);
if ($server->isDirty('status') && $wasInstalling && $server->status === null) {
DB::afterCommit(function () use ($server) {
@@ -77,12 +79,7 @@ protected function registerServerStatusListeners(): void
if (class_exists($eventClass)) {
Event::listen($eventClass, function ($event) use ($webhookEvent) {
$server = $event->server ?? null;
- if ($server instanceof Server) {
- // Only update the cache/state, do not send webhook here
- $cacheKey = "webhook_server_status_{$server->id}";
- $state = $webhookEvent === WebhookEvent::ServerStarted ? 'running' : 'offline';
- \Illuminate\Support\Facades\Cache::put($cacheKey, $state, now()->addHours(24));
- }
+ // Do not update the cache here; let CheckServerStatus handle state and webhook dispatching.
});
}
}