Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add Webhook/ External notification provider #3147

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions app/Jobs/SendMessageToExternalJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class SendMessageToExternalJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 5;

public $backoff = 10;

/**
* The maximum number of unhandled exceptions to allow before failing.
*/
public int $maxExceptions = 5;

public function __construct(
public mixed $payload,
public string $url
) {}

/**
* Execute the job.
*/
public function handle(): void
{
$payload = json_encode($this->payload);
Log::info('Sending ' . $payload . ' to ' . $this->url);
Http::post($this->url, $payload);
}
}
58 changes: 58 additions & 0 deletions app/Livewire/Notifications/External.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace App\Livewire\Notifications;

use App\Models\Team;
use App\Notifications\Test;
use Livewire\Component;

class External extends Component
{
public Team $team;

protected $rules = [
'team.external_enabled' => 'nullable|boolean',
'team.external_url' => 'required|url',
];

public function mount()
{
$this->team = auth()->user()->currentTeam();
}

public function instantSave()
{
try {
$this->submit();
} catch (\Throwable $e) {
ray($e->getMessage());
$this->team->external_enabled = false;
$this->validate();
}
}

public function submit()
{
$this->resetErrorBag();
$this->validate();
$this->saveModel();
}

public function saveModel()
{
$this->team->save();
refreshSession();
$this->dispatch('success', 'Settings saved.');
}

public function sendTestNotification()
{
$this->team?->notify(new Test);
$this->dispatch('success', 'Test notification sent.');
}

public function render()
{
return view('livewire.notifications.external');
}
}
37 changes: 28 additions & 9 deletions app/Models/Team.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Notifications\Channels\SendsDiscord;
use App\Notifications\Channels\SendsEmail;
use App\Notifications\Channels\SendsExternal;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
Expand Down Expand Up @@ -59,6 +60,8 @@
'custom_server_limit' => ['type' => 'string', 'description' => 'The custom server limit.'],
'telegram_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Telegram.'],
'telegram_notifications_scheduled_tasks_thread_id' => ['type' => 'string', 'description' => 'The Telegram scheduled task message thread ID.'],
'team.external_enabled' => ['type' => 'boolean', 'description' => 'Whether external notifications are enabled'],
'team.external_url' => ['type' => 'string', 'description' => 'The external URL to send notifications to'],
'members' => new OA\Property(
property: 'members',
type: 'array',
Expand All @@ -67,7 +70,7 @@
),
]
)]
class Team extends Model implements SendsDiscord, SendsEmail
class Team extends Model implements SendsDiscord, SendsEmail, SendsExternal
{
use Notifiable;

Expand All @@ -90,27 +93,27 @@ protected static function booted()
static::deleting(function ($team) {
$keys = $team->privateKeys;
foreach ($keys as $key) {
ray('Deleting key: '.$key->name);
ray('Deleting key: ' . $key->name);
$key->delete();
}
$sources = $team->sources();
foreach ($sources as $source) {
ray('Deleting source: '.$source->name);
ray('Deleting source: ' . $source->name);
$source->delete();
}
$tags = Tag::whereTeamId($team->id)->get();
foreach ($tags as $tag) {
ray('Deleting tag: '.$tag->name);
ray('Deleting tag: ' . $tag->name);
$tag->delete();
}
$shared_variables = $team->environment_variables();
foreach ($shared_variables as $shared_variable) {
ray('Deleting team shared variable: '.$shared_variable->name);
ray('Deleting team shared variable: ' . $shared_variable->name);
$shared_variable->delete();
}
$s3s = $team->s3s;
foreach ($s3s as $s3) {
ray('Deleting s3: '.$s3->name);
ray('Deleting s3: ' . $s3->name);
$s3->delete();
}
});
Expand All @@ -121,6 +124,11 @@ public function routeNotificationForDiscord()
return data_get($this, 'discord_webhook_url', null);
}

public function externalURL()
{
return data_get($this, 'external_url', null);
}

public function routeNotificationForTelegram()
{
return [
Expand Down Expand Up @@ -284,10 +292,21 @@ public function isAnyNotificationEnabled()
if (isCloud()) {
return true;
}
if ($this->smtp_enabled || $this->resend_enabled || $this->discord_enabled || $this->telegram_enabled || $this->use_instance_email_settings) {
return true;
}

$conditions = [
$this->smtp_enabled,
$this->resend_enabled,
$this->discord_enabled,
$this->telegram_enabled,
$this->use_instance_email_settings,
$this->external_enabled
];

foreach ($conditions as $cond) {
if ($cond) {
return true;
}
}
return false;
}
}
12 changes: 12 additions & 0 deletions app/Notifications/Application/DeploymentFailed.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ public function toMail(): MailMessage
return $mail;
}

public function toExternal(): mixed {
return [
'event' => 'deployment_failed',
'preview' => $this->preview,
'is_preview' => $this->preview != null,
'fqdn' => ($this->preview != null ? $this->preview->fqdn : $this->fqdn),
'name' => $this->application_name,
'deployment_url' => $this->deployment_url,
'deployment_id' => $this->deployment_uuid
];
}

public function toDiscord(): string
{
if ($this->preview) {
Expand Down
13 changes: 13 additions & 0 deletions app/Notifications/Application/DeploymentSuccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ public function toMail(): MailMessage
return $mail;
}

public function toExternal(): mixed {
return [
'event' => 'deployment_success',
'preview' => $this->preview,
'is_preview' => $this->preview != null,
'fqdn' => ($this->preview != null ? $this->preview->fqdn : $this->fqdn),
'name' => $this->application_name,
'deployment_url' => $this->deployment_url,
'deployment_id' => $this->deployment_uuid
];
}


public function toDiscord(): string
{
if ($this->preview) {
Expand Down
8 changes: 8 additions & 0 deletions app/Notifications/Application/StatusChanged.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ public function toMail(): MailMessage
return $mail;
}

public function toExternal(): mixed {
return [
'event' => 'status_changed',
'status' => 'stopped',
'resource_name' => $this->resource_name,
];
}

public function toDiscord(): string
{
$message = 'Coolify: '.$this->resource_name.' has been stopped.
Expand Down
22 changes: 22 additions & 0 deletions app/Notifications/Channels/ExternalChannel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace App\Notifications\Channels;

use App\Jobs\SendMessageToExternalJob;
use Illuminate\Notifications\Notification;

class ExternalChannel
{
/**
* Send the given notification.
*/
public function send(SendsExternal $notifiable, Notification $notification): void
{
$message = $notification->toExternal();
$url = $notifiable->externalURL();
if (! $url) {
return;
}
dispatch(new SendMessageToExternalJob($message, $url));
}
}
8 changes: 8 additions & 0 deletions app/Notifications/Channels/SendsExternal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace App\Notifications\Channels;

interface SendsExternal
{
public function externalURL();
}
8 changes: 8 additions & 0 deletions app/Notifications/Container/ContainerRestarted.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ public function toMail(): MailMessage
return $mail;
}

public function toExternal(): mixed {
return [
'event' => 'status_changed',
'status' => 'restarted',
'resource_name' => $this->name,
];
}

public function toDiscord(): string
{
$message = "Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}";
Expand Down
8 changes: 8 additions & 0 deletions app/Notifications/Container/ContainerStopped.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ public function toDiscord(): string
return $message;
}

public function toExternal(): mixed {
return [
'event' => 'status_changed',
'status' => 'stopped_unexpectedly',
'resource_name' => $this->name,
];
}

public function toTelegram(): array
{
$message = "Coolify: A resource ($this->name) has been stopped unexpectedly on {$this->server->name}";
Expand Down
8 changes: 8 additions & 0 deletions app/Notifications/Database/BackupFailed.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ public function toMail(): MailMessage
return $mail;
}

public function toExternal(): mixed {
return [
'event' => 'backup_failed',
'database_name' => $this->database_name,
'resource_name' => $this->name,
];
}

public function toDiscord(): string
{
return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}";
Expand Down
9 changes: 8 additions & 1 deletion app/Notifications/Database/DailyBackup.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Notifications\Database;

use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\ExternalChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
Expand All @@ -20,7 +21,7 @@ public function __construct(public $databases) {}

public function via(object $notifiable): array
{
return [DiscordChannel::class, TelegramChannel::class, MailChannel::class];
return [DiscordChannel::class, TelegramChannel::class, MailChannel::class, ExternalChannel::class];
}

public function toMail(): MailMessage
Expand All @@ -34,6 +35,12 @@ public function toMail(): MailMessage
return $mail;
}

public function toExternal(): mixed {
return [
'event' => 'backups_complete',
];
}

public function toDiscord(): string
{
return 'Coolify: Daily backup statuses';
Expand Down
12 changes: 12 additions & 0 deletions app/Notifications/Internal/GeneralNotification.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Notifications\Internal;

use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\ExternalChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
Expand All @@ -21,17 +22,28 @@ public function via(object $notifiable): array
$channels = [];
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isExternalEnabled = data_get($notifiable, 'external_enabled');

if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
if ($isExternalEnabled) {
$channels[] = ExternalChannel::class;
}

return $channels;
}

public function toExternal(): mixed {
return [
'event' => 'message',
'message' => $this->message
];
}

public function toDiscord(): string
{
return $this->message;
Expand Down
Loading
Loading