Merge pull request #1073 from coollabsio/notifications

Starts Notifications feature. Missing to send email with runtime configs
This commit is contained in:
Andras Bacsai 2023-06-01 08:23:33 +02:00 committed by GitHub
commit c8f70a4e3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 492 additions and 27 deletions

6
.gitignore vendored
View File

@ -20,10 +20,8 @@ yarn-error.log
/.npm
/.bash_history
/_data
# Temp while developing Proxy deployment
resources/recipes
_testing_hosts/
_volumes/
.lesshst
psysh_history
.psql_history

View File

@ -0,0 +1,75 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use function Termwind\ask;
use function Termwind\render;
use function Termwind\style;
class NotifyDemo extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:demo-notify {channel?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send a demo notification, to a given channel. Run to see options.';
/**
* Execute the console command.
*/
public function handle()
{
$channel = $this->argument('channel');
if (blank($channel)) {
$this->showHelp();
return;
}
ray($channel);
}
private function showHelp()
{
style('coolify')->color('#9333EA');
style('title-box')->apply('mt-1 px-2 py-1 bg-coolify');
render(<<<'HTML'
<div>
<div class="title-box">
Coolify
</div>
<p class="ml-1 mt-1 ">
Demo Notify <strong class="text-coolify">=></strong> Send a demo notification to a given channel.
</p>
<p class="ml-1 mt-1 bg-coolify px-1">
php artisan app:demo-notify {channel}
</p>
<div class="my-1">
<div class="text-yellow-500"> Channels: </div>
<ul class="text-coolify">
<li>email</li>
<li>slack</li>
<li>discord</li>
<li>telegram</li>
</ul>
</div>
</div>
HTML);
ask(<<<'HTML'
<div class="mr-1">
In which manner you wish a <strong class="text-coolify">coolified</strong> notification?
</div>
HTML, ['email', 'slack', 'discord', 'telegram']);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Livewire\Notifications;
use App\Models\Server;
use App\Models\Team;
use App\Notifications\DemoNotification;
use Illuminate\Support\Facades\Notification;
use Livewire\Component;
class DiscordSettings extends Component
{
public Team|Server $model;
protected $rules = [
'model.extra_attributes.discord_webhook' => 'nullable|url',
'model.extra_attributes.discord_active' => 'nullable|boolean',
];
protected $validationAttributes = [
'model.extra_attributes.discord_webhook' => 'Discord Webhook',
];
public function mount($model)
{
//
}
public function submit()
{
$this->resetErrorBag();
$this->validate();
$this->model->save();
if ( is_a($this->model, Team::class)) {
session(['currentTeam' => $this->model]);
}
}
public function sendTestNotification()
{
Notification::send($this->model, new DemoNotification);
}
public function render()
{
return view('livewire.notifications.discord-settings');
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Livewire\Notifications;
use App\Models\Server;
use App\Models\Team;
use App\Notifications\DemoNotification;
use Illuminate\Support\Facades\Notification;
use Livewire\Component;
class EmailSettings extends Component
{
public Team|Server $model;
protected $rules = [
'model.extra_attributes.smtp_active' => 'nullable|boolean',
'model.extra_attributes.from_address' => 'nullable',
'model.extra_attributes.from_name' => 'nullable',
'model.extra_attributes.recipients' => 'nullable',
'model.extra_attributes.smtp_host' => 'nullable',
'model.extra_attributes.smtp_port' => 'nullable',
'model.extra_attributes.smtp_encryption' => 'nullable',
'model.extra_attributes.smtp_username' => 'nullable',
'model.extra_attributes.smtp_password' => 'nullable',
'model.extra_attributes.smtp_timeout' => 'nullable',
];
protected $validationAttributes = [
'model.extra_attributes.from_address' => 'From Address',
'model.extra_attributes.from_name' => 'From Name',
'model.extra_attributes.recipients' => 'Recipients',
'model.extra_attributes.smtp_host' => 'Host',
'model.extra_attributes.smtp_port' => 'Port',
'model.extra_attributes.smtp_encryption' => 'Encryption',
'model.extra_attributes.smtp_username' => 'Username',
'model.extra_attributes.smtp_password' => 'Password',
'model.extra_attributes.smtp_timeout' => 'Timeout',
];
public function mount($model)
{
//
}
public function submit()
{
$this->resetErrorBag();
$this->validate();
$this->model->save();
if ( is_a($this->model, Team::class)) {
session(['currentTeam' => $this->model]);
}
}
public function sendTestNotification()
{
Notification::send($this->model, new DemoNotification);
}
public function render()
{
return view('livewire.notifications.email-settings');
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class SendMessageToDiscordJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 5;
/**
* The maximum number of unhandled exceptions to allow before failing.
*/
public int $maxExceptions = 3;
public function __construct(
public string $text,
public string $webhookUrl
) {}
/**
* Execute the job.
*/
public function handle(): void
{
$payload = [
'content' => $this->text,
];
Http::post($this->webhookUrl, $payload);
}
}

View File

@ -2,28 +2,59 @@
namespace App\Models;
class Team extends BaseModel
use App\Notifications\Channels\SendsCoolifyEmail;
use App\Notifications\Channels\SendsDiscord;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Notifications\Notifiable;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
class Team extends BaseModel implements SendsDiscord, SendsCoolifyEmail
{
use Notifiable;
protected $casts = [
'extra_attributes' => SchemalessAttributes::class,
'personal_team' => 'boolean',
];
protected $fillable = [
'id',
'name',
'personal_team'
'personal_team',
'extra_attributes',
];
public function routeNotificationForDiscord()
{
return $this->extra_attributes->get('discord_webhook');
}
public function routeNotificationForCoolifyEmail()
{
$recipients = $this->extra_attributes->get('recipients', '');
return explode(PHP_EOL, $recipients);
}
public function scopeWithExtraAttributes(): Builder
{
return $this->extra_attributes->modelScope();
}
public function projects()
{
return $this->hasMany(Project::class);
}
public function servers()
{
return $this->hasMany(Server::class);
}
public function applications()
{
return $this->hasManyThrough(Application::class, Project::class);
}
public function privateKeys()
{
return $this->hasMany(PrivateKey::class);

View File

@ -0,0 +1,44 @@
<?php
namespace App\Notifications\Channels;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Mail;
class CoolifyEmailChannel
{
/**
* Send the given notification.
*/
public function send(SendsCoolifyEmail $notifiable, Notification $notification): void
{
$this->bootConfigs($notifiable);
$bcc = $notifiable->routeNotificationForCoolifyEmail();
$mailMessage = $notification->toMail($notifiable);
Mail::send([], [], fn(Message $message) => $message
->from(
$notifiable->extra_attributes?->get('from_address'),
$notifiable->extra_attributes?->get('from_name')
)
->bcc($bcc)
->subject($mailMessage->subject)
->html((string)$mailMessage->render())
);
}
private function bootConfigs($notifiable): void
{
config()->set('mail.mailers.smtp', [
"transport" => "smtp",
"host" => $notifiable->extra_attributes?->get('smtp_host'),
"port" => $notifiable->extra_attributes?->get('smtp_port'),
"encryption" => $notifiable->extra_attributes?->get('smtp_encryption'),
"username" => $notifiable->extra_attributes?->get('smtp_username'),
"password" => $notifiable->extra_attributes?->get('smtp_password'),
"timeout" => $notifiable->extra_attributes?->get('smtp_timeout'),
"local_domain" => null,
]);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Notifications\Channels;
use App\Jobs\SendMessageToDiscordJob;
use Illuminate\Notifications\Notification;
class DiscordChannel
{
/**
* Send the given notification.
*/
public function send(SendsDiscord $notifiable, Notification $notification): void
{
$message = $notification->toDiscord($notifiable);
$webhookUrl = $notifiable->routeNotificationForDiscord();
dispatch(new SendMessageToDiscordJob($message, $webhookUrl));
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Notifications\Channels;
interface SendsCoolifyEmail
{
public function routeNotificationForCoolifyEmail();
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Notifications\Channels;
interface SendsDiscord
{
public function routeNotificationForDiscord();
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Notifications;
use App\Notifications\Channels\CoolifyEmailChannel;
use App\Notifications\Channels\DiscordChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class DemoNotification extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*/
public function __construct()
{
//
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
$channels = [];
$notifiable->extra_attributes?->get('smtp_active') && $channels[] = CoolifyEmailChannel::class;
$notifiable->extra_attributes?->get('discord_active') && $channels[] = DiscordChannel::class;
return $channels;
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Coolify demo notification')
->line('Welcome to Coolify!')
->action('Go to dashboard', url('/'))
->line('We need your attention for disk usage.');
}
public function toDiscord(object $notifiable): string
{
return 'Welcome to Coolify! We need your attention for disk usage. [Go to dashboard]('.url('/').')';
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
//
];
}
}

View File

@ -16,6 +16,7 @@ public function up(): void
$table->string('uuid')->unique();
$table->string('name');
$table->boolean('personal_team')->default(false);
$table->schemalessAttributes('extra_attributes');
$table->timestamps();
});
}

View File

@ -21,8 +21,8 @@ public function up(): void
$table->boolean('do_not_track')->default(false);
$table->boolean('is_auto_update_enabled')->default(true);
$table->boolean('is_registration_enabled')->default(true);
// $table->string('preview_domain_separator')->default('.');
// $table->string('custom_dns_servers')->default('1.1.1.1,8.8.8.8');
// $table->string('preview_domain_separator')->default('.');
// $table->boolean('is_dns_check_enabled')->default(true);
$table->timestamps();
});

View File

@ -62,6 +62,11 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- "./_data/coolify/proxy/testing-host-2:/data/coolify/proxy"
mailpit:
image: 'axllent/mailpit:latest'
ports:
- '${FORWARD_MAILPIT_PORT:-1025}:1025'
- '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025'
buggregator:
image: ghcr.io/buggregator/server:latest
container_name: coolify-debug

View File

@ -0,0 +1 @@
Hello I am an example

View File

@ -0,0 +1,23 @@
<div class="">
<div class="text-xl">Discord</div>
<div class="mt-2"></div>
<form wire:submit.prevent='submit' class="flex flex-col">
<div class="flex flex-col gap-2 xl:flex-row w-96">
<x-inputs.input type="checkbox" id="model.extra_attributes.discord_active" label="Active?" />
</div>
<div class="flex flex-col gap-2 xl:flex-row w-96">
<x-inputs.input id="model.extra_attributes.discord_webhook" label="Discord Webhook" />
</div>
<div>
<x-inputs.button class="w-16 mt-4" type="submit">
Submit
</x-inputs.button>
<x-inputs.button
class="mt-4 btn btn-xs no-animation normal-case text-white btn-primary"
wire:click="sendTestNotification"
>
Send test message
</x-inputs.button>
</div>
</form>
</div>

View File

@ -0,0 +1,45 @@
<div class="mt-10">
<div class="text-xl">E-mail - SMTP</div>
<div class="mt-2"></div>
<form wire:submit.prevent='submit' class="flex flex-col">
<div class="flex flex-col w-96">
<x-inputs.input type="checkbox" id="model.extra_attributes.smtp_active" label="Active?" />
</div>
<div class="flex flex-col gap-2 xl:flex-row">
<div class="flex flex-col w-96">
<x-inputs.textarea
id="model.extra_attributes.recipients"
helper="E-mails, one per line"
label="Recipients" />
</div>
</div>
<div class="flex flex-col gap-2 xl:flex-row">
<div class="flex flex-col w-96">
<x-inputs.input id="model.extra_attributes.smtp_host" label="Host" />
<x-inputs.input id="model.extra_attributes.smtp_port" label="Port" />
<x-inputs.input id="model.extra_attributes.smtp_encryption" label="Encryption" />
</div>
<div class="flex flex-col w-96">
<x-inputs.input id="model.extra_attributes.smtp_username" label="Username" />
<x-inputs.input id="model.extra_attributes.smtp_password" label="Password" />
<x-inputs.input id="model.extra_attributes.smtp_timeout" label="Timeout" />
</div>
<div class="flex flex-col w-96">
<x-inputs.input id="model.extra_attributes.from_address" label="From Address" />
<x-inputs.input id="model.extra_attributes.from_name" label="From Name" />
<x-inputs.input id="model.extra_attributes.test_address" label="Send test e-mails to" />
</div>
</div>
<div class="flex">
<x-inputs.button class="w-16 mt-4" type="submit">
Submit
</x-inputs.button>
<x-inputs.button
class="mt-4 btn btn-xs no-animation normal-case text-white btn-primary"
wire:click="sendTestNotification"
>
Send test message
</x-inputs.button>
</div>
</form>
</div>

View File

@ -1,3 +1,4 @@
<x-layout>
<h1>Settings</h1>
<livewire:settings.form :settings="$settings" />
</x-layout>

View File

@ -1,8 +1,13 @@
<x-layout>
<h1>Teams</h1>
<div class="flex gap-2">
<div>Currently Active Team:</div>
<div class='text-white'>{{ session('currentTeam')->name }}</div>
<div>
<h3>Current Team</h3>
<p>Name: {{ session('currentTeam.name') }}</p>
<livewire:switch-team />
<div class="h-12"></div>
<h3>Notifications</h3>
<livewire:notifications.discord-settings :model="session('currentTeam')" />
<livewire:notifications.email-settings :model="session('currentTeam')" />
<div class="h-12"></div>
</div>
<livewire:switch-team>
</x-layout>

View File

@ -14,19 +14,6 @@
|
*/
// Artisan::command('inspire', function () {
// $activity = Spatie\Activitylog\Models\Activity::latest()->first();
// $this->info(
// collect(
// json_decode(data_get($activity, 'description'), associative: true, flags: JSON_THROW_ON_ERROR)
// )
// ->sortBy('order')
// ->map(fn($i) => $i['output'])
// ->implode("\n")
// );
// })->purpose('Display an inspiring quote');
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');