Events are {resource}.created, .updated, .deleted and .restored for content,
the inbox, redirects, repositories and media, taken from the audit log so the
two agree on what happened. Subscriptions take wildcards. Users, tokens,
webhooks and settings are never sent, nor what the application does by
itself; the inbox's own words only with personal data enabled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
133 lines
3.6 KiB
PHP
133 lines
3.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Somewhere to tell when something happens here.
|
|
*/
|
|
#[Fillable(['name', 'url', 'secret', 'events', 'include_personal_data', 'is_active'])]
|
|
#[Hidden(['secret'])]
|
|
class Webhook extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
/**
|
|
* Events with a meaning of their own, whatever record they are about.
|
|
*/
|
|
public const EVENTS = [
|
|
'application.received',
|
|
'content.published',
|
|
'content.unpublished',
|
|
'review.requested',
|
|
'review.returned',
|
|
];
|
|
|
|
/**
|
|
* Kinds of record whose every change can be subscribed to, as
|
|
* `{resource}.{action}`. Not users, tokens, webhooks or settings: those
|
|
* are the application's security, not its content, and do not leave it.
|
|
*/
|
|
public const RESOURCES = [
|
|
'pages', 'categories', 'tags', 'projects', 'members', 'services',
|
|
'clients', 'testimonials', 'talks', 'technologies', 'calls',
|
|
'redirects', 'repositories', 'media', 'applications',
|
|
];
|
|
|
|
public const ACTIONS = ['created', 'updated', 'deleted', 'restored'];
|
|
|
|
/**
|
|
* Every event that is ever sent.
|
|
*
|
|
* @return array<int, string>
|
|
*/
|
|
public static function events(): array
|
|
{
|
|
$changes = [];
|
|
|
|
foreach (self::RESOURCES as $resource) {
|
|
foreach (self::ACTIONS as $action) {
|
|
$changes[] = "{$resource}.{$action}";
|
|
}
|
|
}
|
|
|
|
return [...self::EVENTS, ...$changes];
|
|
}
|
|
|
|
/**
|
|
* What a webhook may subscribe to: an event, `*`, everything about one
|
|
* kind of record (`projects.*`) or one action on any (`*.deleted`).
|
|
*/
|
|
public static function isSubscribable(string $pattern): bool
|
|
{
|
|
if ($pattern === '*' || in_array($pattern, self::events(), true)) {
|
|
return true;
|
|
}
|
|
|
|
[$resource, $action] = array_pad(explode('.', $pattern, 2), 2, null);
|
|
|
|
return ($action === '*' && in_array($resource, self::RESOURCES, true))
|
|
|| ($resource === '*' && in_array($action, self::ACTIONS, true));
|
|
}
|
|
|
|
/**
|
|
* Switched off after this many failed deliveries in a row, so a dead
|
|
* endpoint does not keep a queue busy forever. Switching it back on is a
|
|
* person's decision.
|
|
*/
|
|
public const MAX_FAILURES = 20;
|
|
|
|
protected $attributes = [
|
|
'include_personal_data' => false,
|
|
'is_active' => true,
|
|
];
|
|
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'secret' => 'encrypted',
|
|
'events' => 'array',
|
|
'include_personal_data' => 'boolean',
|
|
'is_active' => 'boolean',
|
|
'last_delivered_at' => 'datetime',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return HasMany<WebhookDelivery, $this>
|
|
*/
|
|
public function deliveries(): HasMany
|
|
{
|
|
return $this->hasMany(WebhookDelivery::class)->latest('id');
|
|
}
|
|
|
|
public function listensTo(string $event): bool
|
|
{
|
|
foreach ($this->events ?? [] as $pattern) {
|
|
if ($pattern === $event || Str::is($pattern, $event)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param Builder<$this> $query
|
|
* @return Builder<$this>
|
|
*/
|
|
public function scopeActive(Builder $query): Builder
|
|
{
|
|
return $query->where('is_active', true);
|
|
}
|
|
}
|