mirror of
https://github.com/pelican-dev/panel.git
synced 2025-09-09 01:38:37 +02:00
DiscordWebhooks (#1355)
Co-authored-by: notCharles <charles@pelican.dev> Co-authored-by: Lance Pioch <lancepioch@gmail.com> Co-authored-by: Boy132 <mail@boy132.de> Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
This commit is contained in:
parent
21ac75efae
commit
c5aa8a3980
34
app/Enums/WebhookType.php
Normal file
34
app/Enums/WebhookType.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasIcon;
|
||||
|
||||
enum WebhookType: string implements HasColor, HasIcon, HasLabel
|
||||
{
|
||||
case Regular = 'regular';
|
||||
case Discord = 'discord';
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return trans('admin/webhook.' . $this->value);
|
||||
}
|
||||
|
||||
public function getColor(): ?string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Regular => null,
|
||||
self::Discord => 'blurple',
|
||||
};
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Regular => 'tabler-world-www',
|
||||
self::Discord => 'tabler-brand-discord',
|
||||
};
|
||||
}
|
||||
}
|
@ -3,23 +3,40 @@
|
||||
namespace App\Filament\Admin\Resources;
|
||||
|
||||
use App\Filament\Admin\Resources\WebhookResource\Pages;
|
||||
use App\Filament\Admin\Resources\WebhookResource\Pages\EditWebhookConfiguration;
|
||||
use App\Livewire\AlertBanner;
|
||||
use App\Models\WebhookConfiguration;
|
||||
use App\Traits\Filament\CanCustomizePages;
|
||||
use App\Traits\Filament\CanCustomizeRelations;
|
||||
use App\Traits\Filament\CanModifyForm;
|
||||
use App\Traits\Filament\CanModifyTable;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ReplicateAction;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Livewire\Features\SupportEvents\HandlesEvents;
|
||||
use App\Enums\WebhookType;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Livewire\Component as Livewire;
|
||||
|
||||
class WebhookResource extends Resource
|
||||
{
|
||||
@ -27,6 +44,7 @@ class WebhookResource extends Resource
|
||||
use CanCustomizeRelations;
|
||||
use CanModifyForm;
|
||||
use CanModifyTable;
|
||||
use HandlesEvents;
|
||||
|
||||
protected static ?string $model = WebhookConfiguration::class;
|
||||
|
||||
@ -63,6 +81,12 @@ class WebhookResource extends Resource
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
IconColumn::make('type'),
|
||||
TextColumn::make('endpoint')
|
||||
->label(trans('admin/webhook.table.endpoint'))
|
||||
->formatStateUsing(fn (string $state) => str($state)->after('://'))
|
||||
->limit(60)
|
||||
->wrap(),
|
||||
TextColumn::make('description')
|
||||
->label(trans('admin/webhook.table.description')),
|
||||
TextColumn::make('endpoint')
|
||||
@ -70,9 +94,15 @@ class WebhookResource extends Resource
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
->hidden(fn ($record) => static::canEdit($record)),
|
||||
->hidden(fn (WebhookConfiguration $record) => static::canEdit($record)),
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
ReplicateAction::make()
|
||||
->iconButton()
|
||||
->tooltip(trans('filament-actions::replicate.single.label'))
|
||||
->modal(false)
|
||||
->excludeAttributes(['created_at', 'updated_at'])
|
||||
->beforeReplicaSaved(fn (WebhookConfiguration $replica) => $replica->description .= ' Copy ' . now()->format('Y-m-d H:i:s'))
|
||||
->successRedirectUrl(fn (WebhookConfiguration $replica) => EditWebhookConfiguration::getUrl(['record' => $replica])),
|
||||
])
|
||||
->groupedBulkActions([
|
||||
DeleteBulkAction::make(),
|
||||
@ -82,6 +112,12 @@ class WebhookResource extends Resource
|
||||
->emptyStateHeading(trans('admin/webhook.no_webhooks'))
|
||||
->emptyStateActions([
|
||||
CreateAction::make(),
|
||||
])
|
||||
->persistFiltersInSession()
|
||||
->filters([
|
||||
SelectFilter::make('type')
|
||||
->options(WebhookType::class)
|
||||
->attribute('type'),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -89,25 +125,214 @@ class WebhookResource extends Resource
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('endpoint')
|
||||
->label(trans('admin/webhook.endpoint'))
|
||||
->activeUrl()
|
||||
->required(),
|
||||
ToggleButtons::make('type')
|
||||
->live()
|
||||
->inline()
|
||||
->options(WebhookType::class)
|
||||
->default(WebhookType::Regular->value)
|
||||
->afterStateHydrated(function (string $state) {
|
||||
if ($state === WebhookType::Discord->value) {
|
||||
self::sendHelpBanner();
|
||||
}
|
||||
})
|
||||
->afterStateUpdated(function (string $state) {
|
||||
if ($state === WebhookType::Discord->value) {
|
||||
self::sendHelpBanner();
|
||||
}
|
||||
}),
|
||||
TextInput::make('description')
|
||||
->label(trans('admin/webhook.description'))
|
||||
->required(),
|
||||
TextInput::make('endpoint')
|
||||
->label(trans('admin/webhook.endpoint'))
|
||||
->activeUrl()
|
||||
->required()
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (string $state, Set $set) => $set('type', str($state)->contains('discord.com') ? WebhookType::Discord->value : WebhookType::Regular->value)),
|
||||
Section::make(trans('admin/webhook.regular'))
|
||||
->hidden(fn (Get $get) => $get('type') === WebhookType::Discord->value)
|
||||
->dehydratedWhenHidden()
|
||||
->schema(fn () => self::getRegularFields())
|
||||
->formBefore(),
|
||||
Section::make(trans('admin/webhook.discord'))
|
||||
->hidden(fn (Get $get) => $get('type') === WebhookType::Regular->value)
|
||||
->dehydratedWhenHidden()
|
||||
->afterStateUpdated(fn (Livewire $livewire) => $livewire->dispatch('refresh-widget'))
|
||||
->schema(fn () => self::getDiscordFields())
|
||||
->view('filament.components.webhooksection')
|
||||
->aside()
|
||||
->formBefore(),
|
||||
Section::make(trans('admin/webhook.events'))
|
||||
->collapsible()
|
||||
->collapsed(fn (Get $get) => count($get('events') ?? []))
|
||||
->schema([
|
||||
CheckboxList::make('events')
|
||||
->lazy()
|
||||
->live()
|
||||
->options(fn () => WebhookConfiguration::filamentCheckboxList())
|
||||
->searchable()
|
||||
->bulkToggleable()
|
||||
->columns(3)
|
||||
->columnSpanFull()
|
||||
->gridDirection('row')
|
||||
->required(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return Component[] */
|
||||
private static function getRegularFields(): array
|
||||
{
|
||||
return [
|
||||
KeyValue::make('headers')
|
||||
->label(trans('admin/webhook.headers')),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return Component[] */
|
||||
private static function getDiscordFields(): array
|
||||
{
|
||||
return [
|
||||
Section::make(trans('admin/webhook.discord_message.profile'))
|
||||
->collapsible()
|
||||
->schema([
|
||||
TextInput::make('username')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_message.username')),
|
||||
TextInput::make('avatar_url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_message.avatar_url')),
|
||||
]),
|
||||
Section::make(trans('admin/webhook.discord_message.message'))
|
||||
->collapsible()
|
||||
->schema([
|
||||
TextInput::make('content')
|
||||
->label(trans('admin/webhook.discord_message.message'))
|
||||
->live(debounce: 500)
|
||||
->required(fn (Get $get) => empty($get('embeds'))),
|
||||
TextInput::make('thread_name')
|
||||
->label(trans('admin/webhook.discord_message.forum_thread')),
|
||||
CheckboxList::make('flags')
|
||||
->label('Flags')
|
||||
->options([
|
||||
(1 << 2) => trans('admin/webhook.discord_message.supress_embeds'),
|
||||
(1 << 12) => trans('admin/webhook.discord_message.supress_notifications'),
|
||||
])
|
||||
->descriptions([
|
||||
(1 << 2) => trans('admin/webhook.discord_message.supress_embeds_text'),
|
||||
(1 << 12) => trans('admin/webhook.discord_message.supress_notifications_text'),
|
||||
]),
|
||||
CheckboxList::make('allowed_mentions')
|
||||
->label(trans('admin/webhook.discord_embed.allowed_mentions'))
|
||||
->options([
|
||||
'roles' => trans('admin/webhook.discord_embed.roles'),
|
||||
'users' => trans('admin/webhook.discord_embed.users'),
|
||||
'everyone' => trans('admin/webhook.discord_embed.everyone'),
|
||||
]),
|
||||
]),
|
||||
Repeater::make('embeds')
|
||||
->live(debounce: 500)
|
||||
->itemLabel(fn (array $state) => $state['title'])
|
||||
->addActionLabel(trans('admin/webhook.discord_embed.add_embed'))
|
||||
->required(fn (Get $get) => empty($get('content')))
|
||||
->reorderable()
|
||||
->collapsible()
|
||||
->maxItems(10)
|
||||
->schema([
|
||||
Section::make(trans('admin/webhook.discord_embed.author'))
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('author.name')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.author'))
|
||||
->required(fn (Get $get) => filled($get('author.url')) || filled($get('author.icon_url'))),
|
||||
TextInput::make('author.url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.author_url')),
|
||||
TextInput::make('author.icon_url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.author_icon_url')),
|
||||
]),
|
||||
Section::make(trans('admin/webhook.discord_embed.body'))
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.title'))
|
||||
->required(fn (Get $get) => $get('description') === null),
|
||||
Textarea::make('description')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.body'))
|
||||
->required(fn (Get $get) => $get('title') === null),
|
||||
ColorPicker::make('color')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.color'))
|
||||
->hex(),
|
||||
TextInput::make('url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.url')),
|
||||
]),
|
||||
Section::make(trans('admin/webhook.discord_embed.images'))
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('image.url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.image_url')),
|
||||
TextInput::make('thumbnail.url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.image_thumbnail')),
|
||||
]),
|
||||
Section::make(trans('admin/webhook.discord_embed.footer'))
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('footer.text')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.footer')),
|
||||
Checkbox::make('has_timestamp')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.has_timestamp')),
|
||||
TextInput::make('footer.icon_url')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.footer_icon_url')),
|
||||
]),
|
||||
Section::make(trans('admin/webhook.discord_embed.fields'))
|
||||
->collapsible()->collapsed()
|
||||
->schema([
|
||||
Repeater::make('fields')
|
||||
->reorderable()
|
||||
->addActionLabel(trans('admin/webhook.discord_embed.add_field'))
|
||||
->collapsible()
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.field_name'))
|
||||
->required(),
|
||||
Textarea::make('value')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.field_value'))
|
||||
->rows(4)
|
||||
->required(),
|
||||
Checkbox::make('inline')
|
||||
->live(debounce: 500)
|
||||
->label(trans('admin/webhook.discord_embed.inline_field')),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
public static function sendHelpBanner(): void
|
||||
{
|
||||
AlertBanner::make('discord_webhook_help')
|
||||
->title(trans('admin/webhook.help'))
|
||||
->body(trans('admin/webhook.help_text'))
|
||||
->icon('tabler-question-mark')
|
||||
->info()
|
||||
->send();
|
||||
}
|
||||
|
||||
/** @return array<string, PageRegistration> */
|
||||
public static function getDefaultPages(): array
|
||||
{
|
||||
|
@ -8,6 +8,7 @@ use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use App\Enums\WebhookType;
|
||||
|
||||
class CreateWebhookConfiguration extends CreateRecord
|
||||
{
|
||||
@ -22,6 +23,7 @@ class CreateWebhookConfiguration extends CreateRecord
|
||||
protected function getDefaultHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getCancelFormAction()->formId('form'),
|
||||
$this->getCreateFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
@ -30,4 +32,35 @@ class CreateWebhookConfiguration extends CreateRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
if (($data['type'] ?? null) === WebhookType::Discord->value) {
|
||||
$embeds = data_get($data, 'embeds', []);
|
||||
|
||||
foreach ($embeds as &$embed) {
|
||||
$embed['color'] = hexdec(str_replace('#', '', data_get($embed, 'color')));
|
||||
$embed = collect($embed)->filter(fn ($key) => is_array($key) ? array_filter($key, fn ($arr_key) => !empty($arr_key)) : !empty($key))->all();
|
||||
}
|
||||
|
||||
$flags = collect($data['flags'] ?? [])->reduce(fn ($carry, $bit) => $carry | $bit, 0);
|
||||
|
||||
$tmp = collect([
|
||||
'username' => data_get($data, 'username'),
|
||||
'avatar_url' => data_get($data, 'avatar_url'),
|
||||
'content' => data_get($data, 'content'),
|
||||
'image' => data_get($data, 'image'),
|
||||
'thumbnail' => data_get($data, 'thumbnail'),
|
||||
'embeds' => $embeds,
|
||||
'thread_name' => data_get($data, 'thread_name'),
|
||||
'flags' => $flags,
|
||||
'allowed_mentions' => data_get($data, 'allowed_mentions', []),
|
||||
])->filter(fn ($key) => !empty($key))->all();
|
||||
|
||||
unset($data['username'], $data['avatar_url'], $data['content'], $data['image'], $data['thumbnail'], $data['embeds'], $data['thread_name'], $data['flags'], $data['allowed_mentions']);
|
||||
$data['payload'] = $tmp;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,10 @@ use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use App\Models\WebhookConfiguration;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use App\Enums\WebhookType;
|
||||
|
||||
class EditWebhookConfiguration extends EditRecord
|
||||
{
|
||||
@ -22,6 +24,12 @@ class EditWebhookConfiguration extends EditRecord
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
Action::make('test_now')
|
||||
->label(trans('admin/webhook.test_now'))
|
||||
->color('primary')
|
||||
->disabled(fn (WebhookConfiguration $webhookConfiguration) => count($webhookConfiguration->events) === 0)
|
||||
->action(fn (WebhookConfiguration $webhookConfiguration) => $webhookConfiguration->run())
|
||||
->tooltip(trans('admin/webhook.test_now_help')),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
@ -30,4 +38,89 @@ class EditWebhookConfiguration extends EditRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
if (($data['type'] ?? null) === WebhookType::Discord->value) {
|
||||
$embeds = data_get($data, 'embeds', []);
|
||||
|
||||
foreach ($embeds as &$embed) {
|
||||
$embed['color'] = hexdec(str_replace('#', '', data_get($embed, 'color')));
|
||||
$embed = collect($embed)->filter(fn ($key) => is_array($key) ? array_filter($key, fn ($arr_key) => !empty($arr_key)) : !empty($key))->all();
|
||||
}
|
||||
|
||||
$flags = collect($data['flags'] ?? [])->reduce(fn ($carry, $bit) => $carry | $bit, 0);
|
||||
|
||||
$tmp = collect([
|
||||
'username' => data_get($data, 'username'),
|
||||
'avatar_url' => data_get($data, 'avatar_url'),
|
||||
'content' => data_get($data, 'content'),
|
||||
'image' => data_get($data, 'image'),
|
||||
'thumbnail' => data_get($data, 'thumbnail'),
|
||||
'embeds' => $embeds,
|
||||
'thread_name' => data_get($data, 'thread_name'),
|
||||
'flags' => $flags,
|
||||
'allowed_mentions' => data_get($data, 'allowed_mentions', []),
|
||||
])->filter(fn ($key) => !empty($key))->all();
|
||||
|
||||
unset($data['username'], $data['avatar_url'], $data['content'], $data['image'], $data['thumbnail'], $data['embeds'], $data['thread_name'], $data['flags'], $data['allowed_mentions']);
|
||||
|
||||
$data['payload'] = $tmp;
|
||||
}
|
||||
|
||||
if (($data['type'] ?? null) === WebhookType::Regular->value && isset($data['headers']) && is_array($data['headers'])) {
|
||||
$newHeaders = [];
|
||||
foreach ($data['headers'] as $key => $value) {
|
||||
$newKey = str_replace(' ', '-', $key);
|
||||
$newHeaders[$newKey] = $value;
|
||||
}
|
||||
$data['headers'] = $newHeaders;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
if (($data['type'] ?? null) === WebhookType::Discord->value) {
|
||||
$embeds = data_get($data, 'payload.embeds', []);
|
||||
|
||||
foreach ($embeds as &$embed) {
|
||||
$embed['color'] = '#' . dechex(data_get($embed, 'color'));
|
||||
$embed = collect($embed)->filter(fn ($key) => is_array($key) ? array_filter($key, fn ($arr_key) => !empty($arr_key)) : !empty($key))->all();
|
||||
}
|
||||
|
||||
$flags = data_get($data, 'payload.flags');
|
||||
$flags = collect(range(0, PHP_INT_SIZE * 8 - 1))
|
||||
->filter(fn ($i) => ($flags & (1 << $i)) !== 0)
|
||||
->map(fn ($i) => 1 << $i)
|
||||
->values();
|
||||
|
||||
$tmp = collect([
|
||||
'username' => data_get($data, 'payload.username'),
|
||||
'avatar_url' => data_get($data, 'payload.avatar_url'),
|
||||
'content' => data_get($data, 'payload.content'),
|
||||
'image' => data_get($data, 'payload.image'),
|
||||
'thumbnail' => data_get($data, 'payload.thumbnail'),
|
||||
'embeds' => $embeds,
|
||||
'thread_name' => data_get($data, 'payload.thread_name'),
|
||||
'flags' => $flags,
|
||||
'allowed_mentions' => data_get($data, 'payload.allowed_mentions'),
|
||||
])->filter(fn ($key) => !empty($key))->all();
|
||||
|
||||
unset($data['payload'], $data['created_at'], $data['updated_at'], $data['deleted_at']);
|
||||
$data = array_merge($data, $tmp);
|
||||
}
|
||||
|
||||
if (($data['type'] ?? null) === WebhookType::Regular->value) {
|
||||
$data['headers'] = $data['headers'] ?? [];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->dispatch('refresh-widget');
|
||||
}
|
||||
}
|
||||
|
163
app/Filament/Admin/Widgets/DiscordPreview.php
Normal file
163
app/Filament/Admin/Widgets/DiscordPreview.php
Normal file
@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Widgets;
|
||||
|
||||
use App\Models\WebhookConfiguration;
|
||||
use Filament\Widgets\Widget;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class DiscordPreview extends Widget
|
||||
{
|
||||
protected static string $view = 'filament.admin.widgets.discord-preview';
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected $listeners = [
|
||||
'refresh-widget' => '$refresh',
|
||||
];
|
||||
|
||||
protected static bool $isDiscovered = false; // Without this its shown on every Admin Pages
|
||||
|
||||
protected int|string|array $columnSpan = 1;
|
||||
|
||||
public ?WebhookConfiguration $record = null;
|
||||
|
||||
/** @var string|array<string, mixed>|null */
|
||||
public string|array|null $payload = null;
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* link: callable,
|
||||
* content: mixed,
|
||||
* sender: array{name: string, avatar: string},
|
||||
* embeds: array<int, mixed>,
|
||||
* getTime: mixed
|
||||
* }
|
||||
*/
|
||||
public function getViewData(): array
|
||||
{
|
||||
if (!$this->record || !$this->record->payload) {
|
||||
return [
|
||||
'link' => fn ($href, $child) => $href ? "<a href=\"$href\" target=\"_blank\" class=\"link\">$child</a>" : $child,
|
||||
'content' => null,
|
||||
'sender' => [
|
||||
'name' => 'Pelican',
|
||||
'avatar' => 'https://raw.githubusercontent.com/pelican-dev/panel/refs/heads/main/public/pelican.ico',
|
||||
],
|
||||
'embeds' => [],
|
||||
'getTime' => 'Today at ' . Carbon::now()->format('h:i A'),
|
||||
];
|
||||
}
|
||||
|
||||
$data = $this->getWebhookSampleData();
|
||||
|
||||
if (is_string($this->record->payload)) {
|
||||
$payload = $this->replaceVarsInStringPayload($this->record->payload, $data);
|
||||
} else {
|
||||
$payload = $this->replaceVarsInArrayPayload($this->record->payload, $data);
|
||||
}
|
||||
|
||||
$embeds = data_get($payload, 'embeds', []);
|
||||
foreach ($embeds as &$embed) {
|
||||
if (data_get($embed, 'has_timestamp')) {
|
||||
unset($embed['has_timestamp']);
|
||||
$embed['timestamp'] = 'Today at ' . Carbon::now()->format('h:i A');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'link' => fn ($href, $child) => $href ? sprintf('<a href="%s" target="_blank" class="link">%s</a>', $href, $child) : $child,
|
||||
'content' => data_get($payload, 'content'),
|
||||
'sender' => [
|
||||
'name' => data_get($payload, 'username', 'Pelican'),
|
||||
'avatar' => data_get($payload, 'avatar_url', 'https://raw.githubusercontent.com/pelican-dev/panel/refs/heads/main/public/pelican.ico'),
|
||||
],
|
||||
'embeds' => $embeds,
|
||||
'getTime' => 'Today at ' . Carbon::now()->format('h:i A'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function replaceVarsInStringPayload(?string $payload, array $data): ?string
|
||||
{
|
||||
if ($payload === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return preg_replace_callback('/{{\s*([\w\.]+)\s*}}/', fn ($m) => data_get($data, $m[1], $m[0]),
|
||||
$payload
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $payload
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function replaceVarsInArrayPayload(?array $payload, array $data): ?array
|
||||
{
|
||||
if ($payload === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($payload as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
$payload[$key] = $this->replaceVarsInStringPayload($value, $data);
|
||||
} elseif (is_array($value)) {
|
||||
$payload[$key] = $this->replaceVarsInArrayPayload($value, $data);
|
||||
}
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getWebhookSampleData(): array
|
||||
{
|
||||
return [
|
||||
'event' => 'updated: server',
|
||||
'id' => 2,
|
||||
'external_id' => 10,
|
||||
'uuid' => '651fgbc1-dee6-4250-814e-10slda13f1e',
|
||||
'uuid_short' => '651fgbc1',
|
||||
'node_id' => 1,
|
||||
'name' => 'Example Server',
|
||||
'description' => 'This is an example server description.',
|
||||
'status' => 'running',
|
||||
'skip_scripts' => false,
|
||||
'owner_id' => 1,
|
||||
'memory' => 512,
|
||||
'swap' => 128,
|
||||
'disk' => 10240,
|
||||
'io' => 500,
|
||||
'cpu' => 500,
|
||||
'threads' => '1, 3, 5',
|
||||
'oom_killer' => false,
|
||||
'allocation_id' => 4,
|
||||
'egg_id' => 2,
|
||||
'startup' => 'This is a example startup command.',
|
||||
'image' => 'Image here',
|
||||
'allocation_limit' => 5,
|
||||
'database_limit' => 1,
|
||||
'backup_limit' => 3,
|
||||
'created_at' => '2025-03-17T15:20:32.000000Z',
|
||||
'updated_at' => '2025-05-12T17:53:12.000000Z',
|
||||
'installed_at' => '2025-04-27T21:06:01.000000Z',
|
||||
'docker_labels' => [],
|
||||
'allocation' => [
|
||||
'id' => 4,
|
||||
'node_id' => 1,
|
||||
'ip' => '192.168.0.3',
|
||||
'ip_alias' => null,
|
||||
'port' => 25567,
|
||||
'server_id' => 2,
|
||||
'notes' => null,
|
||||
'created_at' => '2025-03-17T15:20:09.000000Z',
|
||||
'updated_at' => '2025-03-17T15:20:32.000000Z',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -3,12 +3,15 @@
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\WebhookConfiguration;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Enums\WebhookType;
|
||||
|
||||
class ProcessWebhook implements ShouldQueue
|
||||
{
|
||||
@ -25,17 +28,45 @@ class ProcessWebhook implements ShouldQueue
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$data = $this->data[0];
|
||||
|
||||
if ($this->webhookConfiguration->type === WebhookType::Discord) {
|
||||
$data = array_merge(
|
||||
is_array($data) ? $data : json_decode($data, true),
|
||||
['event' => $this->webhookConfiguration->transformClassName($this->eventName)]
|
||||
);
|
||||
|
||||
$payload = json_encode($this->webhookConfiguration->payload);
|
||||
$tmp = $this->webhookConfiguration->replaceVars($data, $payload);
|
||||
$data = json_decode($tmp, true);
|
||||
|
||||
$embeds = data_get($data, 'embeds');
|
||||
if ($embeds) {
|
||||
foreach ($embeds as &$embed) {
|
||||
if (data_get($embed, 'has_timestamp')) {
|
||||
$embed['timestamp'] = Carbon::now();
|
||||
unset($embed['has_timestamp']);
|
||||
}
|
||||
}
|
||||
$data['embeds'] = $embeds;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Http::withHeader('X-Webhook-Event', $this->eventName)
|
||||
->post($this->webhookConfiguration->endpoint, $this->data)
|
||||
->throw();
|
||||
$headers = [];
|
||||
if ($this->webhookConfiguration->type === WebhookType::Regular && $customHeaders = $this->webhookConfiguration->headers) {
|
||||
$headers = array_merge(['X-Webhook-Event', $this->eventName], $customHeaders);
|
||||
}
|
||||
|
||||
Http::withHeaders($headers)->post($this->webhookConfiguration->endpoint, $data)->throw();
|
||||
$successful = now();
|
||||
} catch (\Exception) {
|
||||
} catch (Exception $exception) {
|
||||
report($exception->getMessage());
|
||||
$successful = null;
|
||||
}
|
||||
|
||||
$this->webhookConfiguration->webhooks()->create([
|
||||
'payload' => $this->data,
|
||||
'payload' => $data,
|
||||
'successful_at' => $successful,
|
||||
'event' => $this->eventName,
|
||||
'endpoint' => $this->webhookConfiguration->endpoint,
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Jobs\ProcessWebhook;
|
||||
use App\Models\WebhookConfiguration;
|
||||
|
||||
class DispatchWebhooks
|
||||
@ -23,7 +22,7 @@ class DispatchWebhooks
|
||||
/** @var WebhookConfiguration $webhookConfig */
|
||||
foreach ($matchingHooks as $webhookConfig) {
|
||||
if (in_array($eventName, $webhookConfig->events)) {
|
||||
ProcessWebhook::dispatch($webhookConfig, $eventName, $data);
|
||||
$webhookConfig->run($eventName, $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,24 +2,31 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Jobs\ProcessWebhook;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Livewire\Features\SupportEvents\HandlesEvents;
|
||||
use App\Enums\WebhookType;
|
||||
|
||||
/**
|
||||
* @property string|array<string, mixed>|null $payload
|
||||
* @property string $endpoint
|
||||
* @property string $description
|
||||
* @property string[] $events
|
||||
* @property WebhookType|string|null $type
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property array<string, string>|null $headers
|
||||
*/
|
||||
class WebhookConfiguration extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HandlesEvents, HasFactory, SoftDeletes;
|
||||
|
||||
/** @var string[] */
|
||||
protected static array $eventBlacklist = [
|
||||
@ -27,15 +34,29 @@ class WebhookConfiguration extends Model
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'type',
|
||||
'payload',
|
||||
'endpoint',
|
||||
'description',
|
||||
'events',
|
||||
'headers',
|
||||
];
|
||||
|
||||
/**
|
||||
* Default values for specific fields in the database.
|
||||
*/
|
||||
protected $attributes = [
|
||||
'type' => WebhookType::Regular,
|
||||
'payload' => null,
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'events' => 'json',
|
||||
'events' => 'array',
|
||||
'payload' => 'array',
|
||||
'type' => WebhookType::class,
|
||||
'headers' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
@ -43,7 +64,7 @@ class WebhookConfiguration extends Model
|
||||
{
|
||||
self::saved(static function (self $webhookConfiguration): void {
|
||||
$changedEvents = collect([
|
||||
...((array) $webhookConfiguration->events),
|
||||
...($webhookConfiguration->events),
|
||||
...$webhookConfiguration->getOriginal('events', '[]'),
|
||||
])->unique();
|
||||
|
||||
@ -51,7 +72,7 @@ class WebhookConfiguration extends Model
|
||||
});
|
||||
|
||||
self::deleted(static function (self $webhookConfiguration): void {
|
||||
self::updateCache(collect((array) $webhookConfiguration->events));
|
||||
self::updateCache(collect($webhookConfiguration->events));
|
||||
});
|
||||
}
|
||||
|
||||
@ -140,9 +161,7 @@ class WebhookConfiguration extends Model
|
||||
foreach (File::allFiles($directory) as $file) {
|
||||
$namespace = str($file->getPath())
|
||||
->after(base_path())
|
||||
->replace(DIRECTORY_SEPARATOR, '\\')
|
||||
->replace('\\app\\', 'App\\')
|
||||
->toString();
|
||||
->replace([DIRECTORY_SEPARATOR, '\\app\\'], ['\\', 'App\\']);
|
||||
|
||||
$events[] = $namespace . '\\' . str($file->getFilename())
|
||||
->replace([DIRECTORY_SEPARATOR, '.php'], ['\\', '']);
|
||||
@ -150,4 +169,66 @@ class WebhookConfiguration extends Model
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed, mixed> $replacement
|
||||
* */
|
||||
public function replaceVars(array $replacement, string $subject): string
|
||||
{
|
||||
return preg_replace_callback(
|
||||
'/{{(.*?)}}/',
|
||||
function ($matches) use ($replacement) {
|
||||
$trimmed = trim($matches[1]);
|
||||
|
||||
return Arr::get($replacement, $trimmed, $trimmed);
|
||||
},
|
||||
$subject
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<mixed, mixed> $eventData */
|
||||
public function run(?string $eventName = null, ?array $eventData = null): void
|
||||
{
|
||||
$eventName ??= 'eloquent.created: '.Server::class;
|
||||
$eventData ??= $this->getWebhookSampleData();
|
||||
|
||||
ProcessWebhook::dispatch($this, $eventName, [$eventData]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getWebhookSampleData(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'installing',
|
||||
'oom_killer' => false,
|
||||
'installed_at' => null,
|
||||
'external_id' => 10,
|
||||
'uuid' => '651fgbc1-dee6-4250-814e-10slda13f1e',
|
||||
'uuid_short' => '651fgbc1',
|
||||
'node_id' => 1,
|
||||
'name' => 'Eagle',
|
||||
'description' => 'This is an example server description.',
|
||||
'skip_scripts' => false,
|
||||
'owner_id' => 1,
|
||||
'memory' => 2048,
|
||||
'swap' => 128,
|
||||
'disk' => 10240,
|
||||
'io' => 500,
|
||||
'cpu' => 100,
|
||||
'threads' => '1,3,5',
|
||||
'allocation_id' => 4,
|
||||
'egg_id' => 2,
|
||||
'startup' => 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}',
|
||||
'image' => 'ghcr.io/parkervcp/yolks:java_21',
|
||||
'database_limit' => 1,
|
||||
'allocation_limit' => 5,
|
||||
'backup_limit' => 3,
|
||||
'docker_labels' => [],
|
||||
'created_at' => '2025-03-17T15:20:32.000000Z',
|
||||
'updated_at' => '2025-05-12T17:53:12.000000Z',
|
||||
'id' => 2,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
'primary' => Color::Blue,
|
||||
'success' => Color::Green,
|
||||
'warning' => Color::Amber,
|
||||
'blurple' => Color::hex('#5865F2'),
|
||||
]);
|
||||
|
||||
FilamentView::registerRenderHook(
|
||||
|
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\WebhookType;
|
||||
use App\Models\WebhookConfiguration;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('webhook_configurations', function (Blueprint $table) {
|
||||
$table->string('type')->nullable()->after('id');
|
||||
$table->json('payload')->nullable()->after('type');
|
||||
});
|
||||
|
||||
foreach (WebhookConfiguration::all() as $webhookConfig) {
|
||||
$type = str($webhookConfig->endpoint)->contains('discord.com') ? WebhookType::Discord->value : WebhookType::Regular->value;
|
||||
|
||||
DB::table('webhook_configurations')
|
||||
->where('id', $webhookConfig->id)
|
||||
->update(['type' => $type]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('webhook_configurations', function (Blueprint $table) {
|
||||
$table->dropColumn('type');
|
||||
$table->dropColumn('payload');
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('webhook_configurations', function (Blueprint $table) {
|
||||
$table->json('headers')->nullable()->after('payload');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('webhook_configurations', function (Blueprint $table) {
|
||||
$table->dropColumn('headers');
|
||||
});
|
||||
}
|
||||
};
|
@ -8,8 +8,57 @@ return [
|
||||
'description' => 'Description',
|
||||
'events' => 'Events',
|
||||
'no_webhooks' => 'No Webhooks',
|
||||
'help' => 'Help',
|
||||
'help_text' => 'You have to wrap variable name in between {{ }} for example if you want to get the name from the api you can use {{name}}.',
|
||||
'test_now' => 'Test Now',
|
||||
'test_now_help' => 'This will fire a `created: Server` event',
|
||||
'table' => [
|
||||
'description' => 'Description',
|
||||
'endpoint' => 'Endpoint',
|
||||
],
|
||||
'headers' => 'Headers',
|
||||
'events' => 'Events',
|
||||
'regular' => 'Regular',
|
||||
'discord' => 'Discord',
|
||||
'discord_message' => [
|
||||
'profile' => 'Profile',
|
||||
'message' => 'Message',
|
||||
'username' => 'Username',
|
||||
'avatar_url' => 'Avatar URL',
|
||||
'forum_thread' => 'Forum Thread Name',
|
||||
'supress_embeds' => 'Suppress Embeds',
|
||||
'supress_embeds_text' => 'Do not include any embeds when serializing this message',
|
||||
'supress_notifications' => 'Suppress Notifications',
|
||||
'supress_notifications_text' => 'This message will not trigger push and desktop notifications',
|
||||
],
|
||||
'discord_embed' => [
|
||||
'add_embed' => 'Add Embed',
|
||||
'flags' => 'Flags',
|
||||
'thumbnail' => 'Thumbnail URL',
|
||||
'embeds' => 'Embeds',
|
||||
'thread_name' => 'Forum Thread Name',
|
||||
'flags' => 'Flags',
|
||||
'allowed_mentions' => 'Allowed Mentions',
|
||||
'roles' => 'Roles',
|
||||
'users' => 'Users',
|
||||
'everyone' => '@everyone & @here',
|
||||
'author' => 'Author',
|
||||
'author_url' => 'Author URL',
|
||||
'author_icon_url' => 'Author Icon URL',
|
||||
'body' => 'Body',
|
||||
'title' => 'Title',
|
||||
'color' => 'Embed Color',
|
||||
'url' => 'URL',
|
||||
'images' => 'Images',
|
||||
'image_url' => 'Image URL',
|
||||
'image_thumbnail' => 'Thumbnail URL',
|
||||
'footer' => 'Footer',
|
||||
'has_timestamp' => 'Has Timestamp',
|
||||
'footer_icon_url' => 'Footer Icon URL',
|
||||
'add_field' => 'Add Field',
|
||||
'fields' => 'Fields',
|
||||
'field_name' => 'Field Name',
|
||||
'field_value' => 'Field Value',
|
||||
'inline_field' => 'Inline Field',
|
||||
],
|
||||
];
|
||||
|
221
resources/views/filament/admin/widgets/discord-preview.blade.php
Normal file
221
resources/views/filament/admin/widgets/discord-preview.blade.php
Normal file
@ -0,0 +1,221 @@
|
||||
<x-filament-widgets::widget>
|
||||
@assets
|
||||
<style>
|
||||
:root {
|
||||
--discord-embed-background-color: #13162a;
|
||||
--discord-tag-color: #5865f2;
|
||||
--discord-timestamp-color: #949ba4;
|
||||
--discord-text-color: #dbdee1;
|
||||
--discord-link-color: #00a8fc;
|
||||
--discord-avatar-margin-right: 8px;
|
||||
--discord-thumbnail-margin-right: 8px;
|
||||
--discord-footer-margin-top: 8px;
|
||||
--discord-spacer-margin-left: 4px;
|
||||
--discord-avatar-length: 40px;
|
||||
--discord-avatar-height: 40px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #11131f !important;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--discord-link-color);
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
img:hover {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.sender .avatar {
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
.sender .name {
|
||||
display: inline;
|
||||
vertical-align: baseline;
|
||||
margin: 0px 0.25rem 0px 0px;
|
||||
color: var(--color-white);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.375rem;
|
||||
overflow-wrap: break-word;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sender .tag {
|
||||
min-height: 1.275em;
|
||||
max-height: 1.275em;
|
||||
margin: 0.075em 0.25rem 0px 0px;
|
||||
padding: 0.071875rem 0.275rem;
|
||||
border-radius: 3px;
|
||||
background: var(--discord-tag-color);
|
||||
font-size: .8rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.3;
|
||||
vertical-align: baseline;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sender .timestamp {
|
||||
display: inline-block;
|
||||
height: 1.25rem;
|
||||
cursor: default;
|
||||
color: var(--discord-timestamp-color);
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.375rem;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.embed {
|
||||
border-left: 5px solid;
|
||||
background-color: var(--discord-embed-background-color) !important;
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.footer-icon {
|
||||
margin-right: var(--discord-avatar-margin-right);
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-15%);
|
||||
}
|
||||
|
||||
.description,
|
||||
.field-value {
|
||||
color: var(--discord-text-color);
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: var(--discord-footer-margin-top);
|
||||
color: var(--discord-text-color);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
margin-left: var(--discord-spacer-margin-left);
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@endassets
|
||||
<div class="container mx-auto p-4 bg-gray-800 p-4 rounded-lg shadow-lg w-full max-w-full flex items-start mb-4 sender">
|
||||
<div class="relative" style="width: 44px; min-width: 44px; height: 44px; margin-right: 12px;">
|
||||
@if($avatar = $sender['avatar'])
|
||||
<img
|
||||
src="{{ $avatar }}"
|
||||
alt="Avatar"
|
||||
class="w-full h-full rounded-full object-cover absolute top-0 left-0 z-10 avatar"
|
||||
>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-grow">
|
||||
<div class="flex items-center space-x-2">
|
||||
<h1 class="font-bold text-white name">{{ data_get($sender, 'name') }}</h1>
|
||||
@if(!data_get($sender, 'human'))
|
||||
<span class="text-white text-xs rounded-md tag">app</span>
|
||||
@endif
|
||||
<span class="timestamp text-xs">{{ $getTime }}</span>
|
||||
</div>
|
||||
|
||||
@if(filled($content))
|
||||
<p class="text-gray-300 break-words">{!! nl2br($content) !!}</p>
|
||||
@endif
|
||||
|
||||
@foreach($embeds as $embed)
|
||||
@php
|
||||
$name = $embed['author']['name'] ?? null;
|
||||
$thumbnail = $embed['thumbnail']['url'] ?? null;
|
||||
$author_icon_url = $embed['author']['icon_url'] ?? null;
|
||||
$author_url = $embed['author']['url'] ?? null;
|
||||
$footer_icon_url = $embed['footer']['icon_url'] ?? null;
|
||||
$footer_text = $embed['footer']['text'] ?? null;
|
||||
$footer_timestamp = $embed['timestamp'] ?? null;
|
||||
@endphp
|
||||
<div class="p-3 mt-3 rounded-lg w-full max-w-full embed relative" style="border-color: #{{ dechex(data_get($embed, 'color')) }}">
|
||||
@if($name || $thumbnail)
|
||||
<div class="flex items-start mb-0 relative" style="height: auto; overflow: visible;">
|
||||
@if($author_icon_url || $name)
|
||||
<div class="flex items-center">
|
||||
@if($author_icon_url)
|
||||
<img src="{{ $author_icon_url }}" alt="Author Avatar" class="w-8 h-8 rounded-full mr-2 object-cover avatar">
|
||||
@endif
|
||||
@if($author_url)
|
||||
{!! $link($author_url, $name ? '<h2 class="font-bold text-lg whitespace-nowrap">' . e($name) . '</h2>' : '') !!}
|
||||
@elseif($name)
|
||||
<h2 class="font-bold text-lg whitespace-nowrap">{{ $name }}</h2>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@if($thumbnail)
|
||||
<img src="{{ $thumbnail }}" alt="Embed Thumbnail" class="thumbnail rounded-lg">
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($title = data_get($embed, 'title'))
|
||||
{!! $link(
|
||||
$url = data_get($embed, 'url'),
|
||||
'<h3 class="font-bold text-lg break-words mb-0">' . e($title) . '</h3>'
|
||||
) !!}
|
||||
@endif
|
||||
|
||||
@if($description = data_get($embed, 'description'))
|
||||
<p class="break-words description mt-0">{!! nl2br($description) !!}</p>
|
||||
@endif
|
||||
|
||||
@if($fields = data_get($embed, 'fields'))
|
||||
<div class="mt-2 w-full">
|
||||
@foreach($fields as $field)
|
||||
<div class="mb-2 w-full">
|
||||
<strong class="break-words mt-2">{{ data_get($field, 'name') }}</strong>
|
||||
<span class="break-words field-value">{{ data_get($field, 'value') }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($image = data_get($embed, 'image.url'))
|
||||
<img src="{{ $image }}" alt="Embed Image" class="object-contain mt-3 w-full">
|
||||
@endif
|
||||
|
||||
@if($footer_text || $footer_timestamp)
|
||||
<div class="flex items-center text-sm mt-4 footer">
|
||||
@if($footer_icon_url)
|
||||
<img src="{{ $footer_icon_url }}" alt="Footer Icon" class="w-5 h-5 rounded-full mr-2 object-cover footer-icon">
|
||||
@endif
|
||||
|
||||
<div class="flex space-x-1">
|
||||
@if($footer_text)
|
||||
<p class="break-words">{!! nl2br($footer_text) !!}</p>
|
||||
@endif
|
||||
|
||||
@if($footer_timestamp)
|
||||
<span class="timestamp">
|
||||
@if($footer_text)
|
||||
<span class="spacer">•</span>
|
||||
@endif
|
||||
{{ $footer_timestamp }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</x-filament-widgets::widget>
|
26
resources/views/filament/components/webhooksection.blade.php
Normal file
26
resources/views/filament/components/webhooksection.blade.php
Normal file
@ -0,0 +1,26 @@
|
||||
<x-filament::section
|
||||
:aside="$isAside()"
|
||||
:collapsed="$isCollapsed()"
|
||||
:collapsible="$isCollapsible() && (! $isAside)"
|
||||
:compact="$isCompact()"
|
||||
:content-before="$isFormBefore()"
|
||||
:description="$getDescription()"
|
||||
:footer-actions="$getFooterActions()"
|
||||
:footer-actions-alignment="$getFooterActionsAlignment()"
|
||||
:header-actions="$getHeaderActions()"
|
||||
:heading="$getHeading()"
|
||||
:icon="$getIcon()"
|
||||
:icon-color="$getIconColor()"
|
||||
:icon-size="$getIconSize()"
|
||||
:persist-collapsed="$shouldPersistCollapsed()"
|
||||
:attributes="
|
||||
\Filament\Support\prepare_inherited_attributes($attributes)
|
||||
->merge(['id' => $getId()], escape: false)
|
||||
"
|
||||
>
|
||||
<x-slot name="heading">
|
||||
@livewire(App\Filament\Admin\Widgets\DiscordPreview::class, ['record' => $getRecord(), 'pollingInterval' => $pollingInterval ?? null])
|
||||
</x-slot>
|
||||
|
||||
{{ $getChildComponentContainer() }}
|
||||
</x-filament::section>
|
Loading…
x
Reference in New Issue
Block a user