Create one and see its secret once, send a test, change the secret, and read each delivery with what the other end answered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
161 lines
5.0 KiB
PHP
161 lines
5.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Webhooks;
|
|
|
|
use App\Jobs\DeliverWebhook;
|
|
use App\Models\Application;
|
|
use App\Models\ReviewComment;
|
|
use App\Models\Revision;
|
|
use App\Models\Webhook;
|
|
use App\Models\WebhookDelivery;
|
|
use App\Support\AdminContent;
|
|
use App\Support\ContentTypes;
|
|
use App\Support\SiteUrls;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Turning what happens here into deliveries for whoever listens.
|
|
*
|
|
* Fed by model events rather than by the controllers, so a page published from
|
|
* the panel, the API or a batch is announced the same way.
|
|
*/
|
|
final class Webhooks
|
|
{
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
* @param array<string, mixed> $personal Sent only to webhooks that opted into personal data.
|
|
*/
|
|
public static function dispatch(string $event, array $data, array $personal = []): void
|
|
{
|
|
$webhooks = Webhook::query()->active()->get()->filter(fn (Webhook $w) => $w->listensTo($event));
|
|
|
|
foreach ($webhooks as $webhook) {
|
|
$delivery = $webhook->deliveries()->create([
|
|
'uuid' => (string) Str::uuid(),
|
|
'event' => $event,
|
|
'payload' => $webhook->include_personal_data ? [...$data, ...$personal] : $data,
|
|
]);
|
|
|
|
DeliverWebhook::dispatch($delivery)->afterCommit();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A `ping`, to check the receiving end before anything real happens.
|
|
*/
|
|
public static function ping(Webhook $webhook): WebhookDelivery
|
|
{
|
|
$delivery = $webhook->deliveries()->create([
|
|
'uuid' => (string) Str::uuid(),
|
|
'event' => 'ping',
|
|
'payload' => ['webhook' => $webhook->id, 'events' => $webhook->events],
|
|
]);
|
|
|
|
DeliverWebhook::dispatch($delivery);
|
|
|
|
return $delivery->refresh();
|
|
}
|
|
|
|
/**
|
|
* One sent again, as a new delivery of the same event and payload.
|
|
*/
|
|
public static function redeliver(WebhookDelivery $delivery): WebhookDelivery
|
|
{
|
|
$again = $delivery->webhook->deliveries()->create([
|
|
'uuid' => (string) Str::uuid(),
|
|
'event' => $delivery->event,
|
|
'payload' => $delivery->payload,
|
|
]);
|
|
|
|
DeliverWebhook::dispatch($again);
|
|
|
|
return $again->refresh();
|
|
}
|
|
|
|
public static function applicationReceived(Application $application): void
|
|
{
|
|
self::dispatch('application.received', [
|
|
'id' => $application->id,
|
|
'kind' => $application->kind->value,
|
|
'call_id' => $application->call_id,
|
|
'subject' => $application->subject(),
|
|
'locale' => $application->locale,
|
|
'created_at' => $application->created_at?->toIso8601String(),
|
|
], [
|
|
'name' => $application->name,
|
|
'email' => $application->email,
|
|
'organisation' => $application->organisation,
|
|
'message' => $application->message,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Publishing and unpublishing are already told apart by the history, for
|
|
* every kind of record and from wherever it happened; this listens there.
|
|
*/
|
|
public static function revisionRecorded(Revision $revision): void
|
|
{
|
|
$model = Relation::getMorphedModel($revision->revisionable_type) ?? $revision->revisionable_type;
|
|
$record = $model::query()->find($revision->revisionable_id);
|
|
|
|
if ($record === null) {
|
|
return;
|
|
}
|
|
|
|
$event = match (true) {
|
|
$revision->event === 'published' => 'content.published',
|
|
$revision->event === 'unpublished' => 'content.unpublished',
|
|
$revision->event === 'created' && method_exists($record, 'isPublishedNow') && $record->isPublishedNow() => 'content.published',
|
|
default => null,
|
|
};
|
|
|
|
if ($event !== null) {
|
|
self::dispatch($event, self::describe($record));
|
|
}
|
|
}
|
|
|
|
public static function reviewRecorded(ReviewComment $comment): void
|
|
{
|
|
$event = match ($comment->event->value) {
|
|
'requested' => 'review.requested',
|
|
'returned' => 'review.returned',
|
|
default => null,
|
|
};
|
|
|
|
$record = $comment->reviewable;
|
|
|
|
if ($event === null || $record === null) {
|
|
return;
|
|
}
|
|
|
|
self::dispatch($event, [
|
|
...self::describe($record),
|
|
'by' => $comment->user_name,
|
|
'note' => $comment->body,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected static function describe(Model $record): array
|
|
{
|
|
$type = AdminContent::nameOf($record);
|
|
$siteType = AdminContent::PREVIEWABLE[$type] ?? null;
|
|
$locale = (string) config('locales.default');
|
|
|
|
return [
|
|
'type' => $type,
|
|
'id' => $record->getKey(),
|
|
'title' => ContentTypes::title($record),
|
|
'url' => $siteType && method_exists($record, 'getTranslation')
|
|
? (SiteUrls::absoluteAlternates($record, $siteType)[$locale] ?? null)
|
|
: null,
|
|
];
|
|
}
|
|
}
|