diff --git a/app/Actions/Notifications/BuildProjectNotificationSettingsPayload.php b/app/Actions/Notifications/BuildProjectNotificationSettingsPayload.php index 21053a2..19d9cf8 100644 --- a/app/Actions/Notifications/BuildProjectNotificationSettingsPayload.php +++ b/app/Actions/Notifications/BuildProjectNotificationSettingsPayload.php @@ -2,6 +2,7 @@ namespace App\Actions\Notifications; +use App\Enums\DiscordAllowedMentionType; use App\Enums\NotificationChannel; use App\Enums\NotificationEventType; use App\Enums\NotificationFilterAttribute; @@ -26,6 +27,7 @@ class BuildProjectNotificationSettingsPayload 'eventTypeOptions' => NotificationEventType::options(), 'filterAttributeOptions' => NotificationFilterAttribute::options(), 'filterOperatorOptions' => NotificationFilterOperator::options(), + 'discordAllowedMentionOptions' => DiscordAllowedMentionType::options(), 'notificationActions' => NotificationActionResource::collection( $this->listProjectNotificationActions->handle($project), )->resolve(), diff --git a/app/Actions/Notifications/BuildTicketCreatedPayload.php b/app/Actions/Notifications/BuildTicketCreatedPayload.php index 924990c..0bdb9fc 100644 --- a/app/Actions/Notifications/BuildTicketCreatedPayload.php +++ b/app/Actions/Notifications/BuildTicketCreatedPayload.php @@ -2,7 +2,6 @@ namespace App\Actions\Notifications; -use App\Enums\NotificationEventType; use App\Models\Project; use App\Models\Ticket; use App\Models\TicketStatus; @@ -17,9 +16,8 @@ class BuildTicketCreatedPayload TicketStatus $ticketStatus, ?User $actor, CarbonImmutable $occurredAt, - ): TicketStatusChangedPayload { - return new TicketStatusChangedPayload( - eventType: NotificationEventType::TicketCreate, + ): TicketCreatedPayload { + return new TicketCreatedPayload( projectId: $project->id, projectKey: $project->key, projectName: $project->name, @@ -29,8 +27,6 @@ class BuildTicketCreatedPayload ticketUrl: route('projects.tickets.show', [$project, $ticket]), actorId: $actor?->id, actorName: $actor?->name, - fromStatusId: null, - fromStatusLabel: null, toStatusId: $ticketStatus->id, toStatusLabel: $this->statusLabel($ticketStatus), occurredAt: $occurredAt, diff --git a/app/Actions/Notifications/BuildTicketStatusChangedPayload.php b/app/Actions/Notifications/BuildTicketStatusChangedPayload.php index b923fed..96e2ebd 100644 --- a/app/Actions/Notifications/BuildTicketStatusChangedPayload.php +++ b/app/Actions/Notifications/BuildTicketStatusChangedPayload.php @@ -2,7 +2,6 @@ namespace App\Actions\Notifications; -use App\Enums\NotificationEventType; use App\Models\Project; use App\Models\Ticket; use App\Models\TicketStatus; @@ -22,7 +21,6 @@ class BuildTicketStatusChangedPayload CarbonImmutable $occurredAt, ): TicketStatusChangedPayload { return new TicketStatusChangedPayload( - eventType: NotificationEventType::TicketStatusChanged, projectId: $project->id, projectKey: $project->key, projectName: $project->name, diff --git a/app/Actions/Notifications/DeliverNotificationAction.php b/app/Actions/Notifications/DeliverNotificationAction.php index 8cdaef5..d614767 100644 --- a/app/Actions/Notifications/DeliverNotificationAction.php +++ b/app/Actions/Notifications/DeliverNotificationAction.php @@ -2,6 +2,7 @@ namespace App\Actions\Notifications; +use App\Enums\DiscordAllowedMentionType; use App\Enums\NotificationChannel; use App\Enums\NotificationDeliveryStatus; use App\Models\NotificationAction; @@ -27,7 +28,7 @@ class DeliverNotificationAction implements ShouldQueue public function __construct( public int $notificationActionId, - public TicketStatusChangedPayload $payload, + public NotificationEvent $event, public ?int $notificationDeliveryLogId, ) {} @@ -59,7 +60,7 @@ class DeliverNotificationAction implements ShouldQueue if ($notificationAction->channel === NotificationChannel::Discord) { $discordMessage = app(RenderDiscordMessageTemplate::class)->handle( $notificationAction->message_template, - $this->payload, + $this->event, ); $validationError = $this->discordMessageValidationError($discordMessage); @@ -244,7 +245,7 @@ class DeliverNotificationAction implements ShouldQueue 'attempt_count' => 0, 'channel' => $notificationAction->channel, 'delivery_status' => NotificationDeliveryStatus::Pending, - 'event_type' => $this->payload->eventType, + 'event_type' => $this->event->eventType(), 'notification_action_id' => $notificationAction->id, 'payload_metadata' => $this->payloadMetadata(), ]); @@ -280,18 +281,60 @@ class DeliverNotificationAction implements ShouldQueue ->post($notificationAction->delivery_url, [ 'content' => $message, 'allowed_mentions' => [ - 'parse' => [], + 'parse' => $this->discordAllowedMentions($notificationAction), ], ]); } + /** + * @return list + */ + private function discordAllowedMentions(NotificationAction $notificationAction): array + { + $configuredAllowedMentions = data_get( + $notificationAction->channel_configuration, + 'allowed_mentions', + [], + ); + + if (! is_array($configuredAllowedMentions)) { + return []; + } + + $allowedMentions = []; + + foreach ($configuredAllowedMentions as $allowedMention) { + if (! is_string($allowedMention)) { + continue; + } + + $allowedMention = Str::lower(trim($allowedMention)); + + if ($allowedMention === '') { + continue; + } + + if (! in_array($allowedMention, DiscordAllowedMentionType::values(), true)) { + continue; + } + + if (in_array($allowedMention, $allowedMentions, true)) { + continue; + } + + $allowedMentions[] = $allowedMention; + } + + return $allowedMentions; + } + private function sendWebhookRequest(NotificationAction $notificationAction): Response { return Http::acceptJson() ->asJson() ->connectTimeout(5) ->timeout(10) - ->post($notificationAction->delivery_url, $this->payload->toArray()); + ->post($notificationAction->delivery_url, $this->event->payload()->toArray()); } private function hasAttemptsRemaining(int $attemptCount): bool @@ -355,14 +398,11 @@ class DeliverNotificationAction implements ShouldQueue } /** - * @return array{project_key: string, ticket_key: string} + * @return array */ private function payloadMetadata(): array { - return [ - 'project_key' => $this->payload->projectKey, - 'ticket_key' => $this->payload->ticketKey, - ]; + return $this->event->payload()->deliveryMetadata(); } private function excerpt(?string $value): ?string diff --git a/app/Actions/Notifications/MatchNotificationFilters.php b/app/Actions/Notifications/MatchNotificationFilters.php index e273931..68ac489 100644 --- a/app/Actions/Notifications/MatchNotificationFilters.php +++ b/app/Actions/Notifications/MatchNotificationFilters.php @@ -10,7 +10,7 @@ class MatchNotificationFilters /** * @param array $filters */ - public function handle(array $filters, TicketStatusChangedPayload $payload): bool + public function handle(array $filters, NotificationEventPayload $payload): bool { if ($filters === []) { return true; @@ -175,32 +175,46 @@ class MatchNotificationFilters /** * @return array */ - private function payloadContext(TicketStatusChangedPayload $payload): array + private function payloadContext(NotificationEventPayload $payload): array { + $projectId = property_exists($payload, 'projectId') ? $payload->projectId : $payload->projectId(); + $projectKey = property_exists($payload, 'projectKey') ? $payload->projectKey : null; + $projectName = property_exists($payload, 'projectName') ? $payload->projectName : null; + $ticketId = property_exists($payload, 'ticketId') ? $payload->ticketId : null; + $ticketKey = property_exists($payload, 'ticketKey') ? $payload->ticketKey : null; + $ticketTitle = property_exists($payload, 'ticketTitle') ? $payload->ticketTitle : null; + $ticketUrl = property_exists($payload, 'ticketUrl') ? $payload->ticketUrl : null; + $actorId = property_exists($payload, 'actorId') ? $payload->actorId : null; + $actorName = property_exists($payload, 'actorName') ? $payload->actorName : null; + $fromStatusId = property_exists($payload, 'fromStatusId') ? $payload->fromStatusId : null; + $fromStatusLabel = property_exists($payload, 'fromStatusLabel') ? $payload->fromStatusLabel : null; + $toStatusId = property_exists($payload, 'toStatusId') ? $payload->toStatusId : null; + $toStatusLabel = property_exists($payload, 'toStatusLabel') ? $payload->toStatusLabel : null; + return [ 'project' => [ - 'id' => $payload->projectId, - 'key' => $payload->projectKey, - 'name' => $payload->projectName, + 'id' => $projectId, + 'key' => $projectKey, + 'name' => $projectName, ], 'ticket' => [ - 'id' => $payload->ticketId, - 'key' => $payload->ticketKey, - 'title' => $payload->ticketTitle, - 'url' => $payload->ticketUrl, + 'id' => $ticketId, + 'key' => $ticketKey, + 'title' => $ticketTitle, + 'url' => $ticketUrl, ], 'actor' => [ - 'id' => $payload->actorId, - 'name' => $payload->actorName, + 'id' => $actorId, + 'name' => $actorName, ], 'status' => [ 'from' => [ - 'id' => $payload->fromStatusId, - 'label' => $payload->fromStatusLabel, + 'id' => $fromStatusId, + 'label' => $fromStatusLabel, ], 'to' => [ - 'id' => $payload->toStatusId, - 'label' => $payload->toStatusLabel, + 'id' => $toStatusId, + 'label' => $toStatusLabel, ], ], ]; diff --git a/app/Actions/Notifications/NotificationEvent.php b/app/Actions/Notifications/NotificationEvent.php new file mode 100644 index 0000000..0af8390 --- /dev/null +++ b/app/Actions/Notifications/NotificationEvent.php @@ -0,0 +1,12 @@ + + */ +interface NotificationEventPayload extends Arrayable +{ + public function projectId(): int; + + /** + * @return array + */ + public function deliveryMetadata(): array; + + /** + * @return array + */ + public function discordTemplateVariables(): array; +} diff --git a/app/Actions/Notifications/QueueTicketStatusChangedNotificationDeliveries.php b/app/Actions/Notifications/QueueNotificationEventDeliveries.php similarity index 68% rename from app/Actions/Notifications/QueueTicketStatusChangedNotificationDeliveries.php rename to app/Actions/Notifications/QueueNotificationEventDeliveries.php index 1458ef5..c7fa854 100644 --- a/app/Actions/Notifications/QueueTicketStatusChangedNotificationDeliveries.php +++ b/app/Actions/Notifications/QueueNotificationEventDeliveries.php @@ -7,24 +7,26 @@ use App\Enums\NotificationDeliveryStatus; use App\Models\NotificationAction; use App\Models\NotificationDeliveryLog; -class QueueTicketStatusChangedNotificationDeliveries +class QueueNotificationEventDeliveries { public function __construct( private readonly MatchNotificationFilters $matchNotificationFilters, ) {} - public function handle(TicketStatusChanged|TicketCreated $event): void + public function handle(NotificationEvent $event): void { + $payload = $event->payload(); + $eventType = $event->eventType(); + NotificationAction::query() ->enabled() - ->where('project_id', $event->payload->projectId) - ->where('event_type', $event->payload->eventType->value) + ->where('project_id', $payload->projectId()) + ->where('event_type', $eventType->value) ->get(['channel', 'filters', 'id']) - ->each(function (NotificationAction $notificationAction) use ($event): void { - if (! $this->matchNotificationFilters->handle($notificationAction->filters ?? [], $event->payload)) { + ->each(function (NotificationAction $notificationAction) use ($event, $payload): void { + if (! $this->matchNotificationFilters->handle($notificationAction->filters ?? [], $payload)) { return; } - $notificationDeliveryLogId = null; if (in_array($notificationAction->channel, [NotificationChannel::Discord, NotificationChannel::Webhook], true)) { @@ -32,18 +34,15 @@ class QueueTicketStatusChangedNotificationDeliveries 'attempt_count' => 0, 'channel' => $notificationAction->channel, 'delivery_status' => NotificationDeliveryStatus::Pending, - 'event_type' => $event->payload->eventType, + 'event_type' => $event->eventType(), 'notification_action_id' => $notificationAction->id, - 'payload_metadata' => [ - 'project_key' => $event->payload->projectKey, - 'ticket_key' => $event->payload->ticketKey, - ], + 'payload_metadata' => $payload->deliveryMetadata(), ])->id; } DeliverNotificationAction::dispatch( notificationActionId: $notificationAction->id, - payload: $event->payload, + event: $event, notificationDeliveryLogId: $notificationDeliveryLogId, ); }); diff --git a/app/Actions/Notifications/RenderDiscordMessageTemplate.php b/app/Actions/Notifications/RenderDiscordMessageTemplate.php index e06bd20..aafd4e0 100644 --- a/app/Actions/Notifications/RenderDiscordMessageTemplate.php +++ b/app/Actions/Notifications/RenderDiscordMessageTemplate.php @@ -6,16 +6,8 @@ use Illuminate\Support\Str; class RenderDiscordMessageTemplate { - public function handle(?string $messageTemplate, TicketStatusChangedPayload $payload): string + public function handle(?string $messageTemplate, NotificationEvent $event): string { - return strtr((string) Str::of((string) $messageTemplate), [ - '$url' => $payload->ticketUrl, - '$ticket_key' => $payload->ticketKey, - '$ticket_title' => $payload->ticketTitle, - '$project_name' => $payload->projectName, - '$status_from' => $payload->fromStatusLabel ?? '', - '$status_to' => $payload->toStatusLabel, - '$actor_name' => $payload->actorName ?? '', - ]); + return strtr((string) Str::of((string) $messageTemplate), $event->payload()->discordTemplateVariables()); } } diff --git a/app/Actions/Notifications/TicketCreated.php b/app/Actions/Notifications/TicketCreated.php index 0a50410..62bf62c 100644 --- a/app/Actions/Notifications/TicketCreated.php +++ b/app/Actions/Notifications/TicketCreated.php @@ -2,14 +2,25 @@ namespace App\Actions\Notifications; +use App\Enums\NotificationEventType; use Illuminate\Contracts\Events\ShouldDispatchAfterCommit; use Illuminate\Foundation\Events\Dispatchable; -final readonly class TicketCreated implements ShouldDispatchAfterCommit +final readonly class TicketCreated implements NotificationEvent, ShouldDispatchAfterCommit { use Dispatchable; public function __construct( - public TicketStatusChangedPayload $payload, + public TicketCreatedPayload $payload, ) {} + + public function eventType(): NotificationEventType + { + return NotificationEventType::TicketCreate; + } + + public function payload(): NotificationEventPayload + { + return $this->payload; + } } diff --git a/app/Actions/Notifications/TicketCreatedPayload.php b/app/Actions/Notifications/TicketCreatedPayload.php new file mode 100644 index 0000000..f9373f0 --- /dev/null +++ b/app/Actions/Notifications/TicketCreatedPayload.php @@ -0,0 +1,105 @@ +fromStatusId = null; + $this->fromStatusLabel = null; + } + + public function projectId(): int + { + return $this->projectId; + } + + /** + * @return array + */ + public function deliveryMetadata(): array + { + return [ + 'project_key' => $this->projectKey, + 'ticket_key' => $this->ticketKey, + ]; + } + + /** + * @return array + */ + public function discordTemplateVariables(): array + { + return [ + '$url' => $this->ticketUrl, + '$ticket_key' => $this->ticketKey, + '$ticket_title' => $this->ticketTitle, + '$project_name' => $this->projectName, + '$status_from' => '', + '$status_to' => $this->toStatusLabel, + '$actor_name' => $this->actorName ?? '', + ]; + } + + /** + * @return array{ + * event_type: string, + * project: array{id: int, key: string, name: string}, + * ticket: array{id: int, key: string, title: string}, + * actor: array{id: int, name: string}|null, + * from: null, + * to: array{id: int, label: string}, + * ticket_url: string, + * occurred_at: string + * } + */ + public function toArray(): array + { + return [ + 'event_type' => NotificationEventType::TicketCreate->value, + 'project' => [ + 'id' => $this->projectId, + 'key' => $this->projectKey, + 'name' => $this->projectName, + ], + 'ticket' => [ + 'id' => $this->ticketId, + 'key' => $this->ticketKey, + 'title' => $this->ticketTitle, + ], + 'actor' => $this->actorId === null || $this->actorName === null + ? null + : [ + 'id' => $this->actorId, + 'name' => $this->actorName, + ], + 'from' => null, + 'to' => [ + 'id' => $this->toStatusId, + 'label' => $this->toStatusLabel, + ], + 'ticket_url' => $this->ticketUrl, + 'occurred_at' => $this->occurredAt->toISOString(), + ]; + } +} diff --git a/app/Actions/Notifications/TicketStatusChanged.php b/app/Actions/Notifications/TicketStatusChanged.php index 245d0b7..41e2aa5 100644 --- a/app/Actions/Notifications/TicketStatusChanged.php +++ b/app/Actions/Notifications/TicketStatusChanged.php @@ -2,14 +2,25 @@ namespace App\Actions\Notifications; +use App\Enums\NotificationEventType; use Illuminate\Contracts\Events\ShouldDispatchAfterCommit; use Illuminate\Foundation\Events\Dispatchable; -final readonly class TicketStatusChanged implements ShouldDispatchAfterCommit +final readonly class TicketStatusChanged implements NotificationEvent, ShouldDispatchAfterCommit { use Dispatchable; public function __construct( public TicketStatusChangedPayload $payload, ) {} + + public function eventType(): NotificationEventType + { + return NotificationEventType::TicketStatusChanged; + } + + public function payload(): NotificationEventPayload + { + return $this->payload; + } } diff --git a/app/Actions/Notifications/TicketStatusChangedPayload.php b/app/Actions/Notifications/TicketStatusChangedPayload.php index 87cdd22..cc6997b 100644 --- a/app/Actions/Notifications/TicketStatusChangedPayload.php +++ b/app/Actions/Notifications/TicketStatusChangedPayload.php @@ -4,15 +4,10 @@ namespace App\Actions\Notifications; use App\Enums\NotificationEventType; use Carbon\CarbonImmutable; -use Illuminate\Contracts\Support\Arrayable; -/** - * @implements Arrayable - */ -final readonly class TicketStatusChangedPayload implements Arrayable +final readonly class TicketStatusChangedPayload implements NotificationEventPayload { public function __construct( - public NotificationEventType $eventType, public int $projectId, public string $projectKey, public string $projectName, @@ -22,20 +17,52 @@ final readonly class TicketStatusChangedPayload implements Arrayable public string $ticketUrl, public ?int $actorId, public ?string $actorName, - public ?int $fromStatusId, - public ?string $fromStatusLabel, + public int $fromStatusId, + public string $fromStatusLabel, public int $toStatusId, public string $toStatusLabel, public CarbonImmutable $occurredAt, ) {} + public function projectId(): int + { + return $this->projectId; + } + + /** + * @return array + */ + public function deliveryMetadata(): array + { + return [ + 'project_key' => $this->projectKey, + 'ticket_key' => $this->ticketKey, + ]; + } + + /** + * @return array + */ + public function discordTemplateVariables(): array + { + return [ + '$url' => $this->ticketUrl, + '$ticket_key' => $this->ticketKey, + '$ticket_title' => $this->ticketTitle, + '$project_name' => $this->projectName, + '$status_from' => $this->fromStatusLabel, + '$status_to' => $this->toStatusLabel, + '$actor_name' => $this->actorName ?? '', + ]; + } + /** * @return array{ * event_type: string, * project: array{id: int, key: string, name: string}, * ticket: array{id: int, key: string, title: string}, * actor: array{id: int, name: string}|null, - * from: array{id: int, label: string}|null, + * from: array{id: int, label: string}, * to: array{id: int, label: string}, * ticket_url: string, * occurred_at: string @@ -44,7 +71,7 @@ final readonly class TicketStatusChangedPayload implements Arrayable public function toArray(): array { return [ - 'event_type' => $this->eventType->value, + 'event_type' => NotificationEventType::TicketStatusChanged->value, 'project' => [ 'id' => $this->projectId, 'key' => $this->projectKey, @@ -61,12 +88,10 @@ final readonly class TicketStatusChangedPayload implements Arrayable 'id' => $this->actorId, 'name' => $this->actorName, ], - 'from' => $this->fromStatusId === null || $this->fromStatusLabel === null - ? null - : [ - 'id' => $this->fromStatusId, - 'label' => $this->fromStatusLabel, - ], + 'from' => [ + 'id' => $this->fromStatusId, + 'label' => $this->fromStatusLabel, + ], 'to' => [ 'id' => $this->toStatusId, 'label' => $this->toStatusLabel, diff --git a/app/Enums/DiscordAllowedMentionType.php b/app/Enums/DiscordAllowedMentionType.php new file mode 100644 index 0000000..50ea956 --- /dev/null +++ b/app/Enums/DiscordAllowedMentionType.php @@ -0,0 +1,45 @@ + '@everyone and @here', + self::Roles => 'Role mentions (@role)', + self::Users => 'User mentions (@username)', + }; + } + + /** + * @return array + */ + public static function options(): array + { + return array_map( + fn (self $eventType): array => [ + 'label' => $eventType->label(), + 'value' => $eventType->value, + ], + self::cases(), + ); + } + + /** + * @param array|null $discordAllowedMentionTypes + * @return array + */ + public static function values(?array $discordAllowedMentionTypes = null): array + { + return array_map( + fn (self $discordAllowedMentionType): string => $discordAllowedMentionType->value, + $discordAllowedMentionTypes ?? self::cases(), + ); + } +} diff --git a/app/Enums/NotificationChannel.php b/app/Enums/NotificationChannel.php index a869ce7..902f647 100644 --- a/app/Enums/NotificationChannel.php +++ b/app/Enums/NotificationChannel.php @@ -28,4 +28,16 @@ enum NotificationChannel: string self::cases(), ); } + + /** + * @param array|null $notificationChannels + * @return array + */ + public static function values(?array $notificationChannels = null): array + { + return array_map( + fn (self $notificationChannel): string => $notificationChannel->value, + $notificationChannels ?? self::cases(), + ); + } } diff --git a/app/Enums/NotificationEventType.php b/app/Enums/NotificationEventType.php index 39d0c20..629e2af 100644 --- a/app/Enums/NotificationEventType.php +++ b/app/Enums/NotificationEventType.php @@ -28,4 +28,16 @@ enum NotificationEventType: string self::cases(), ); } + + /** + * @param array|null $notificationEventTypes + * @return array + */ + public static function values(?array $notificationEventTypes = null): array + { + return array_map( + fn (self $notificationEventType): string => $notificationEventType->value, + $notificationEventTypes ?? self::cases(), + ); + } } diff --git a/app/Http/Requests/StoreProjectNotificationActionRequest.php b/app/Http/Requests/StoreProjectNotificationActionRequest.php index 715716d..590f2f9 100644 --- a/app/Http/Requests/StoreProjectNotificationActionRequest.php +++ b/app/Http/Requests/StoreProjectNotificationActionRequest.php @@ -2,6 +2,7 @@ namespace App\Http\Requests; +use App\Enums\DiscordAllowedMentionType; use App\Enums\NotificationChannel; use App\Enums\NotificationEventType; use App\Http\Requests\Concerns\InteractsWithNotificationFilters; @@ -29,10 +30,17 @@ class StoreProjectNotificationActionRequest extends FormRequest */ public function rules(): array { + $messageTemplateRules = $this->input('channel') === NotificationChannel::Discord->value + ? ['required', 'string', 'max:2000'] + : ['nullable', 'string', 'max:2000']; + return [ 'name' => ['required', 'string', 'max:255'], - 'event_type' => ['required', 'string', Rule::in($this->eventTypes())], - 'channel' => ['required', 'string', Rule::in($this->channels())], + 'event_type' => ['required', 'string', Rule::in(NotificationEventType::values())], + 'channel' => ['required', 'string', Rule::in(NotificationChannel::values())], + 'channel_configuration' => ['nullable', 'array'], + 'channel_configuration.allowed_mentions' => ['nullable', 'array'], + 'channel_configuration.allowed_mentions.*' => ['nullable', 'string', Rule::in(DiscordAllowedMentionType::values())], 'enabled' => ['required', 'boolean'], 'delivery_url' => ['required', 'string', 'max:2048', 'url'], 'message_template' => $this->messageTemplateRules(), diff --git a/app/Http/Requests/UpdateProjectNotificationActionRequest.php b/app/Http/Requests/UpdateProjectNotificationActionRequest.php index f4fb6f2..0178abd 100644 --- a/app/Http/Requests/UpdateProjectNotificationActionRequest.php +++ b/app/Http/Requests/UpdateProjectNotificationActionRequest.php @@ -2,6 +2,7 @@ namespace App\Http\Requests; +use App\Enums\DiscordAllowedMentionType; use App\Enums\NotificationChannel; use App\Enums\NotificationEventType; use App\Http\Requests\Concerns\InteractsWithNotificationFilters; @@ -29,10 +30,17 @@ class UpdateProjectNotificationActionRequest extends FormRequest */ public function rules(): array { + $messageTemplateRules = $this->input('channel') === NotificationChannel::Discord->value + ? ['required', 'string', 'max:2000'] + : ['nullable', 'string', 'max:2000']; + return [ 'name' => ['required', 'string', 'max:255'], - 'event_type' => ['required', 'string', Rule::in($this->eventTypes())], - 'channel' => ['required', 'string', Rule::in($this->channels())], + 'event_type' => ['required', 'string', Rule::in(NotificationEventType::values())], + 'channel' => ['required', 'string', Rule::in(NotificationChannel::values())], + 'channel_configuration' => ['nullable', 'array'], + 'channel_configuration.allowed_mentions' => ['nullable', 'array'], + 'channel_configuration.allowed_mentions.*' => ['nullable', 'string', Rule::in(DiscordAllowedMentionType::values())], 'enabled' => ['required', 'boolean'], 'delivery_url' => ['nullable', 'string', 'max:2048', 'url'], 'message_template' => $this->messageTemplateRules(), diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index fd02c24..c8685f8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,7 +2,7 @@ namespace App\Providers; -use App\Actions\Notifications\QueueTicketStatusChangedNotificationDeliveries; +use App\Actions\Notifications\QueueNotificationEventDeliveries; use App\Actions\Notifications\TicketCreated; use App\Actions\Notifications\TicketStatusChanged; use App\Actions\Permissions\PermissionService; @@ -98,12 +98,12 @@ class AppServiceProvider extends ServiceProvider { Event::listen( TicketStatusChanged::class, - QueueTicketStatusChangedNotificationDeliveries::class, + QueueNotificationEventDeliveries::class, ); Event::listen( TicketCreated::class, - QueueTicketStatusChangedNotificationDeliveries::class, + QueueNotificationEventDeliveries::class, ); } } diff --git a/resources/js/pages/projects/settings/Notifications.vue b/resources/js/pages/projects/settings/Notifications.vue index 410a323..e0740b5 100644 --- a/resources/js/pages/projects/settings/Notifications.vue +++ b/resources/js/pages/projects/settings/Notifications.vue @@ -192,6 +192,41 @@ props.notificationActions.forEach((notificationAction) => { const selectedChannel = (notificationAction: NotificationActionModel): string => editChannelState[notificationAction.id] ?? notificationAction.channel; +const notificationActionAllowedMentions = ( + notificationAction: NotificationActionModel, +): string[] => { + const allowedMentions = + notificationAction.channel_configuration['allowed_mentions']; + + if (!Array.isArray(allowedMentions)) { + return []; + } + + return allowedMentions + .filter( + (allowedMention): allowedMention is string => + typeof allowedMention === 'string', + ) + .map((allowedMention) => allowedMention.trim().toLowerCase()) + .filter((allowedMention) => allowedMention !== ''); +}; + +const allowedMentionsError = ( + errors: Record, +): string | undefined => { + const generalError = errors['channel_configuration.allowed_mentions']; + + if (generalError !== undefined) { + return generalError; + } + + const itemErrorKey = Object.keys(errors).find((key) => + key.startsWith('channel_configuration.allowed_mentions.'), + ); + + return itemErrorKey === undefined ? undefined : errors[itemErrorKey]; +}; + const notificationActionTimestamp = ( notificationAction: NotificationActionModel, ): string => { @@ -336,7 +371,9 @@ const notificationActionTimestamp = (
-
+ +
+ +
+ +
+

+ {{ + createChannelState.value === + 'discord' + ? 'Pick which Discord mention types are allowed. Leave all unchecked to keep mentions fully disabled.' + : 'Allowed mentions are only used when the channel is Discord.' + }} +

+
@@ -661,7 +748,11 @@ const notificationActionTimestamp = (
- {{ notificationActionTimestamp(notificationAction) }} + {{ + notificationActionTimestamp( + notificationAction, + ) + }} @@ -673,25 +764,19 @@ const notificationActionTimestamp = ( [project.id, notificationAction.id], ) " - :error-bag=" - `updateNotificationAction-${notificationAction.id}` - " + :error-bag="`updateNotificationAction-${notificationAction.id}`" class="space-y-4" v-slot="{ errors, processing }" >
+ +
+ +
+

+ {{ + selectedChannel( + notificationAction, + ) === 'discord' + ? 'Pick which Discord mention types are allowed. Leave all unchecked to keep mentions fully disabled.' + : 'Allowed mentions are only used when the channel is Discord.' + }} +

+ +
+