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. }); } }