diff --git a/app/Http/Livewire/Notifications/DiscordSettings.php b/app/Http/Livewire/Notifications/DiscordSettings.php index f9de6d662..cd7a413a5 100644 --- a/app/Http/Livewire/Notifications/DiscordSettings.php +++ b/app/Http/Livewire/Notifications/DiscordSettings.php @@ -8,26 +8,30 @@ class DiscordSettings extends Component { - public Team $model; + public Team $team; protected $rules = [ - 'model.discord_enabled' => 'nullable|boolean', - 'model.discord_webhook_url' => 'required|url', - 'model.discord_notifications_test' => 'nullable|boolean', - 'model.discord_notifications_deployments' => 'nullable|boolean', - 'model.discord_notifications_status_changes' => 'nullable|boolean', - 'model.discord_notifications_database_backups' => 'nullable|boolean', + 'team.discord_enabled' => 'nullable|boolean', + 'team.discord_webhook_url' => 'required|url', + 'team.discord_notifications_test' => 'nullable|boolean', + 'team.discord_notifications_deployments' => 'nullable|boolean', + 'team.discord_notifications_status_changes' => 'nullable|boolean', + 'team.discord_notifications_database_backups' => 'nullable|boolean', ]; protected $validationAttributes = [ - 'model.discord_webhook_url' => 'Discord Webhook', + 'team.discord_webhook_url' => 'Discord Webhook', ]; + public function mount() + { + $this->team = auth()->user()->currentTeam(); + } public function instantSave() { try { $this->submit(); } catch (\Exception $e) { ray($e->getMessage()); - $this->model->discord_enabled = false; + $this->team->discord_enabled = false; $this->validate(); } } @@ -41,8 +45,8 @@ public function submit() public function saveModel() { - $this->model->save(); - if (is_a($this->model, Team::class)) { + $this->team->save(); + if (is_a($this->team, Team::class)) { refreshSession(); } $this->emit('success', 'Settings saved.'); @@ -50,7 +54,7 @@ public function saveModel() public function sendTestNotification() { - $this->model->notify(new Test); + $this->team->notify(new Test()); $this->emit('success', 'Test notification sent.'); } } diff --git a/app/Http/Livewire/Notifications/EmailSettings.php b/app/Http/Livewire/Notifications/EmailSettings.php index bf805e5ec..4ddfaf87f 100644 --- a/app/Http/Livewire/Notifications/EmailSettings.php +++ b/app/Http/Livewire/Notifications/EmailSettings.php @@ -49,6 +49,7 @@ class EmailSettings extends Component public function mount() { + $this->team = auth()->user()->currentTeam(); ['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits; $this->emails = auth()->user()->email; } diff --git a/app/Http/Livewire/Notifications/TelegramSettings.php b/app/Http/Livewire/Notifications/TelegramSettings.php new file mode 100644 index 000000000..be7081012 --- /dev/null +++ b/app/Http/Livewire/Notifications/TelegramSettings.php @@ -0,0 +1,62 @@ + 'nullable|boolean', + 'team.telegram_token' => 'required|string', + 'team.telegram_chat_id' => 'required|string', + 'team.telegram_notifications_test' => 'nullable|boolean', + 'team.telegram_notifications_deployments' => 'nullable|boolean', + 'team.telegram_notifications_status_changes' => 'nullable|boolean', + 'team.telegram_notifications_database_backups' => 'nullable|boolean', + ]; + protected $validationAttributes = [ + 'team.telegram_token' => 'Token', + 'team.telegram_chat_id' => 'Chat ID', + ]; + + public function mount() + { + $this->team = auth()->user()->currentTeam(); + } + public function instantSave() + { + try { + $this->submit(); + } catch (\Exception $e) { + ray($e->getMessage()); + $this->team->telegram_enabled = false; + $this->validate(); + } + } + + public function submit() + { + $this->resetErrorBag(); + $this->validate(); + $this->saveModel(); + } + + public function saveModel() + { + $this->team->save(); + if (is_a($this->team, Team::class)) { + refreshSession(); + } + $this->emit('success', 'Settings saved.'); + } + + public function sendTestNotification() + { + $this->team->notify(new Test()); + $this->emit('success', 'Test notification sent.'); + } +} diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php new file mode 100644 index 000000000..08afbab23 --- /dev/null +++ b/app/Jobs/SendMessageToTelegramJob.php @@ -0,0 +1,72 @@ +token . '/sendMessage'; + $inlineButtons = []; + if (!empty($this->buttons)) { + foreach ($this->buttons as $button) { + $buttonUrl = data_get($button, 'url'); + if ($buttonUrl && Str::contains($buttonUrl, 'http://localhost')) { + $buttonUrl = str_replace('http://localhost', config('app.url'), $buttonUrl); + } + $inlineButtons[] = [ + 'text' => $button['text'], + 'url' => $buttonUrl, + ]; + } + } + $payload = [ + 'parse_mode' => 'markdown', + 'reply_markup' => json_encode([ + 'inline_keyboard' => [ + [...$inlineButtons], + ], + ]), + 'chat_id' => $this->chatId, + 'text' => $this->text, + ]; + ray($payload); + $response = Http::post($url, $payload); + if ($response->failed()) { + throw new \Exception('Telegram notification failed with ' . $response->status() . ' status code.' . $response->body()); + } + } +} diff --git a/app/Models/Team.php b/app/Models/Team.php index b75772569..2d7ef3c9e 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -24,6 +24,14 @@ public function routeNotificationForDiscord() return data_get($this, 'discord_webhook_url', null); } + public function routeNotificationForTelegram() + { + return [ + "token" => data_get($this, 'telegram_token', null), + "chat_id" => data_get($this, 'telegram_chat_id', null) + ]; + } + public function getRecepients($notification) { $recipients = data_get($notification, 'emails', null); diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index b244f4be8..ddbdf1446 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -43,19 +43,7 @@ public function __construct(Application $application, string $deployment_uuid, ? public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_deployments'); - $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_deployments'); - - if ($isEmailEnabled && $isSubscribedToEmailEvent) { - $channels[] = EmailChannel::class; - } - if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { - $channels[] = DiscordChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'deployments'); } public function toMail(): MailMessage @@ -89,4 +77,19 @@ public function toDiscord(): string } return $message; } + public function toTelegram(): array + { + if ($this->preview) { + $message = '❌ Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: '; + } else { + $message = '❌ Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): '; + } + return [ + "message" => $message, + "buttons" => [ + "text" => "View Deployment Logs", + "url" => $this->deployment_url + ], + ]; + } } diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 99bd2532c..f46c44c0e 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -43,19 +43,7 @@ public function __construct(Application $application, string $deployment_uuid, A public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = data_get($notifiable, 'smtp_enabled'); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_deployments'); - $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_deployments'); - - if ($isEmailEnabled && $isSubscribedToEmailEvent) { - $channels[] = EmailChannel::class; - } - if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { - $channels[] = DiscordChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'deployments'); } public function toMail(): MailMessage @@ -99,4 +87,34 @@ public function toDiscord(): string } return $message; } + public function toTelegram(): array + { + if ($this->preview) { + $message = '✅ New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . ''; + if ($this->preview->fqdn) { + $buttons[] = [ + "text" => "Open Application", + "url" => $this->preview->fqdn + ]; + } + } else { + $message = '✅ New version successfully deployed of ' . $this->application_name . ''; + if ($this->fqdn) { + $buttons[] = [ + "text" => "Open Application", + "url" => $this->fqdn + ]; + } + } + $buttons[] = [ + "text" => "Deployment logs", + "url" => $this->deployment_url + ]; + return [ + "message" => $message, + "buttons" => [ + ...$buttons + ], + ]; + } } diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index 9c6b99fc7..0d01d08ab 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -37,19 +37,7 @@ public function __construct($application) public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = data_get($notifiable, 'smtp_enabled'); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_status_changes'); - $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_status_changes'); - - if ($isEmailEnabled && $isSubscribedToEmailEvent) { - $channels[] = EmailChannel::class; - } - if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { - $channels[] = DiscordChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'status_changes'); } public function toMail(): MailMessage @@ -70,7 +58,20 @@ public function toDiscord(): string $message = '⛔ ' . $this->application_name . ' has been stopped. '; - $message .= '[Application URL](' . $this->application_url . ')'; + $message .= '[Open Application in Coolify](' . $this->application_url . ')'; return $message; } + public function toTelegram(): array + { + $message = '⛔ ' . $this->application_name . ' has been stopped.'; + return [ + "message" => $message, + "buttons" => [ + [ + "text" => "Open Application in Coolify", + "url" => $this->application_url + ] + ], + ]; + } } diff --git a/app/Notifications/Channels/SendsTelegram.php b/app/Notifications/Channels/SendsTelegram.php new file mode 100644 index 000000000..ee8bd0656 --- /dev/null +++ b/app/Notifications/Channels/SendsTelegram.php @@ -0,0 +1,9 @@ +toTelegram($notifiable); + $telegramData = $notifiable->routeNotificationForTelegram(); + + $message = data_get($data, 'message'); + $buttons = data_get($data, 'buttons', []); + ray($message, $buttons); + $telegramToken = data_get($telegramData, 'token'); + $chatId = data_get($telegramData, 'chat_id'); + + if (!$telegramToken || !$chatId || !$message) { + throw new \Exception('Telegram token, chat id and message are required'); + } + dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId)); + } +} diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index 79fd9405d..613b0846c 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -25,19 +25,7 @@ public function __construct(ScheduledDatabaseBackup $backup, public $database, p public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups'); - $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups'); - - if ($isEmailEnabled && $isSubscribedToEmailEvent) { - $channels[] = EmailChannel::class; - } - if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { - $channels[] = DiscordChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'database_backups'); } public function toMail(): MailMessage @@ -56,4 +44,11 @@ public function toDiscord(): string { return "❌ Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}"; } + public function toTelegram(): array + { + $message = "❌ Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}"; + return [ + "message" => $message, + ]; + } } diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index 1b279d632..eb6d07c25 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -25,19 +25,7 @@ public function __construct(ScheduledDatabaseBackup $backup, public $database) public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups'); - $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups'); - - if ($isEmailEnabled && $isSubscribedToEmailEvent) { - $channels[] = EmailChannel::class; - } - if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { - $channels[] = DiscordChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'database_backups'); } public function toMail(): MailMessage @@ -55,4 +43,11 @@ public function toDiscord(): string { return "✅ Database backup for {$this->name} with frequency of {$this->frequency} was successful."; } + public function toTelegram(): array + { + $message = "✅ Database backup for {$this->name} with frequency of {$this->frequency} was successful."; + return [ + "message" => $message, + ]; + } } diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php index 3fee2acab..78a76c059 100644 --- a/app/Notifications/Internal/GeneralNotification.php +++ b/app/Notifications/Internal/GeneralNotification.php @@ -3,6 +3,7 @@ namespace App\Notifications\Internal; use App\Notifications\Channels\DiscordChannel; +use App\Notifications\Channels\TelegramChannel; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Notification; @@ -17,11 +18,17 @@ public function __construct(public string $message) public function via(object $notifiable): array { - return [DiscordChannel::class]; + return [TelegramChannel::class, DiscordChannel::class]; } public function toDiscord(): string { return $this->message; } + public function toTelegram(): array + { + return [ + "message" => $this->message, + ]; + } } diff --git a/app/Notifications/Server/NotReachable.php b/app/Notifications/Server/NotReachable.php index bc97d033e..083808224 100644 --- a/app/Notifications/Server/NotReachable.php +++ b/app/Notifications/Server/NotReachable.php @@ -22,19 +22,7 @@ public function __construct(public Server $server) public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_status_changes'); - $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_status_changes'); - - // if ($isEmailEnabled && $isSubscribedToEmailEvent) { - // $channels[] = EmailChannel::class; - // } - if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { - $channels[] = DiscordChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'status_changes'); } public function toMail(): MailMessage @@ -55,4 +43,10 @@ public function toDiscord(): string $message = '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.'; return $message; } + public function toTelegram(): array + { + return [ + "message" => '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.' + ]; + } } diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index 36f0e1053..685434cb2 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -4,6 +4,7 @@ use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\TelegramChannel; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -19,18 +20,7 @@ public function __construct(public string|null $emails = null) public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - - if ($isDiscordEnabled && empty($this->emails)) { - $channels[] = DiscordChannel::class; - } - - if ($isEmailEnabled && !empty($this->emails)) { - $channels[] = EmailChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'test'); } public function toMail(): MailMessage @@ -48,4 +38,16 @@ public function toDiscord(): string $message .= '[Go to your dashboard](' . base_url() . ')'; return $message; } + public function toTelegram(): array + { + return [ + "message" => 'This is a test Telegram notification from Coolify.', + "buttons" => [ + [ + "text" => "Go to your dashboard", + "url" => 'https://coolify.io' + ] + ], + ]; + } } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 1d3dba13d..ac8beecf3 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2,6 +2,9 @@ use App\Models\InstanceSettings; use App\Models\Team; +use App\Notifications\Channels\DiscordChannel; +use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\TelegramChannel; use App\Notifications\Internal\GeneralNotification; use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException; use Illuminate\Database\QueryException; @@ -242,10 +245,9 @@ function validate_cron_expression($expression_to_validate): bool function send_internal_notification(string $message): void { try { - ray('Sending internal notification... 📬 ' . $message); $baseUrl = config('app.name'); $team = Team::find(0); - $team->notify(new GeneralNotification("👀 Internal notifications from {$baseUrl}: " . $message)); + $team->notify(new GeneralNotification("👀 {$baseUrl}: " . $message)); } catch (\Throwable $th) { ray($th->getMessage()); } @@ -270,3 +272,23 @@ function isEmailEnabled($notifiable) { return data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') || data_get($notifiable, 'use_instance_email_settings'); } +function setNotificationChannels($notifiable, $event) +{ + $channels = []; + $isEmailEnabled = isEmailEnabled($notifiable); + $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); + $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); + $isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event"); + $isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event"); + + if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { + $channels[] = DiscordChannel::class; + } + if ($isEmailEnabled) { + $channels[] = EmailChannel::class; + } + if ($isTelegramEnabled && $isSubscribedToTelegramEvent) { + $channels[] = TelegramChannel::class; + } + return $channels; +} diff --git a/database/migrations/2023_08_22_071055_add_discord_notifications_to_teams.php b/database/migrations/2023_08_22_071055_add_discord_notifications_to_teams.php new file mode 100644 index 000000000..0439cae54 --- /dev/null +++ b/database/migrations/2023_08_22_071055_add_discord_notifications_to_teams.php @@ -0,0 +1,34 @@ +boolean('telegram_enabled')->default(false); + $table->text('telegram_token')->nullable(); + $table->text('telegram_chat_id')->nullable(); + $table->boolean('telegram_notifications_test')->default(true); + $table->boolean('telegram_notifications_deployments')->default(true); + $table->boolean('telegram_notifications_status_changes')->default(true); + $table->boolean('telegram_notifications_database_backups')->default(true); + }); + } + + public function down(): void + { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn('telegram_enabled'); + $table->dropColumn('telegram_token'); + $table->dropColumn('telegram_chat_id'); + $table->dropColumn('telegram_notifications_test'); + $table->dropColumn('telegram_notifications_deployments'); + $table->dropColumn('telegram_notifications_status_changes'); + $table->dropColumn('telegram_notifications_database_backups'); + }); + } +}; diff --git a/resources/views/components/helper.blade.php b/resources/views/components/helper.blade.php index 35d22c2f3..df37bf086 100644 --- a/resources/views/components/helper.blade.php +++ b/resources/views/components/helper.blade.php @@ -5,8 +5,8 @@ d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"> -