feat(demo): add story 1 — Sorano: Rock and Time
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Grav\Plugin\Api\Webhooks;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use RocketTheme\Toolbox\File\YamlFile;
|
||||
|
||||
class WebhookManager
|
||||
{
|
||||
private string $storagePath;
|
||||
private string $deliveryPath;
|
||||
private ?array $webhooksCache = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$this->storagePath = $grav['locator']->findResource('user://data/api', true, true);
|
||||
$this->deliveryPath = $this->storagePath . '/webhook-deliveries';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all webhooks.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a webhook by ID.
|
||||
*/
|
||||
public function get(string $id): ?array
|
||||
{
|
||||
$webhooks = $this->load();
|
||||
foreach ($webhooks as $webhook) {
|
||||
if ($webhook['id'] === $id) {
|
||||
return $webhook;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new webhook.
|
||||
*/
|
||||
public function create(array $data): array
|
||||
{
|
||||
$webhook = [
|
||||
'id' => 'wh_' . bin2hex(random_bytes(12)),
|
||||
'url' => $data['url'],
|
||||
'secret' => 'whsec_' . bin2hex(random_bytes(24)),
|
||||
'events' => $data['events'] ?? ['*'],
|
||||
'enabled' => $data['enabled'] ?? true,
|
||||
'headers' => $data['headers'] ?? [],
|
||||
'created' => time(),
|
||||
'failure_count' => 0,
|
||||
];
|
||||
|
||||
$webhooks = $this->load();
|
||||
$webhooks[] = $webhook;
|
||||
$this->save($webhooks);
|
||||
|
||||
return $webhook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a webhook.
|
||||
*/
|
||||
public function update(string $id, array $data): ?array
|
||||
{
|
||||
$webhooks = $this->load();
|
||||
|
||||
foreach ($webhooks as &$webhook) {
|
||||
if ($webhook['id'] === $id) {
|
||||
if (isset($data['url'])) {
|
||||
$webhook['url'] = $data['url'];
|
||||
}
|
||||
if (isset($data['events'])) {
|
||||
$webhook['events'] = $data['events'];
|
||||
}
|
||||
if (isset($data['enabled'])) {
|
||||
$webhook['enabled'] = (bool) $data['enabled'];
|
||||
}
|
||||
if (isset($data['headers'])) {
|
||||
$webhook['headers'] = $data['headers'];
|
||||
}
|
||||
|
||||
$this->save($webhooks);
|
||||
return $webhook;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a webhook.
|
||||
*/
|
||||
public function delete(string $id): bool
|
||||
{
|
||||
$webhooks = $this->load();
|
||||
$filtered = array_values(array_filter($webhooks, fn($w) => $w['id'] !== $id));
|
||||
|
||||
if (count($filtered) === count($webhooks)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->save($filtered);
|
||||
|
||||
// Clean up delivery logs
|
||||
$deliveryDir = $this->deliveryPath . '/' . $id;
|
||||
if (is_dir($deliveryDir)) {
|
||||
$files = glob($deliveryDir . '/*.yaml');
|
||||
foreach ($files as $file) {
|
||||
@unlink($file);
|
||||
}
|
||||
@rmdir($deliveryDir);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a delivery log entry.
|
||||
*/
|
||||
public function recordDelivery(string $webhookId, array $delivery): void
|
||||
{
|
||||
$dir = $this->deliveryPath . '/' . $webhookId;
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$file = $dir . '/' . $delivery['id'] . '.yaml';
|
||||
$yamlFile = YamlFile::instance($file);
|
||||
$yamlFile->content($delivery);
|
||||
$yamlFile->save();
|
||||
|
||||
// Keep only last 50 deliveries
|
||||
$this->pruneDeliveries($webhookId, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get delivery history for a webhook.
|
||||
*/
|
||||
public function getDeliveries(string $webhookId, int $limit = 20, int $offset = 0): array
|
||||
{
|
||||
$dir = $this->deliveryPath . '/' . $webhookId;
|
||||
if (!is_dir($dir)) {
|
||||
return ['deliveries' => [], 'total' => 0];
|
||||
}
|
||||
|
||||
$files = glob($dir . '/*.yaml');
|
||||
// Sort by modification time descending
|
||||
usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a));
|
||||
|
||||
$total = count($files);
|
||||
$slice = array_slice($files, $offset, $limit);
|
||||
|
||||
$deliveries = [];
|
||||
foreach ($slice as $file) {
|
||||
$yamlFile = YamlFile::instance($file);
|
||||
$deliveries[] = $yamlFile->content();
|
||||
}
|
||||
|
||||
return ['deliveries' => $deliveries, 'total' => $total];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get webhooks matching a specific event.
|
||||
*/
|
||||
public function getForEvent(string $event): array
|
||||
{
|
||||
$webhooks = $this->load();
|
||||
return array_filter($webhooks, function ($webhook) use ($event) {
|
||||
if (!($webhook['enabled'] ?? true)) {
|
||||
return false;
|
||||
}
|
||||
$events = $webhook['events'] ?? ['*'];
|
||||
return in_array('*', $events, true) || in_array($event, $events, true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment failure count and auto-disable if threshold reached.
|
||||
*/
|
||||
public function recordFailure(string $id): void
|
||||
{
|
||||
$webhooks = $this->load();
|
||||
foreach ($webhooks as &$webhook) {
|
||||
if ($webhook['id'] === $id) {
|
||||
$webhook['failure_count'] = ($webhook['failure_count'] ?? 0) + 1;
|
||||
if ($webhook['failure_count'] >= 5) {
|
||||
$webhook['enabled'] = false;
|
||||
$webhook['disabled_reason'] = 'Auto-disabled after 5 consecutive failures';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->save($webhooks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset failure count on successful delivery.
|
||||
*/
|
||||
public function resetFailureCount(string $id): void
|
||||
{
|
||||
$webhooks = $this->load();
|
||||
foreach ($webhooks as &$webhook) {
|
||||
if ($webhook['id'] === $id) {
|
||||
$webhook['failure_count'] = 0;
|
||||
unset($webhook['disabled_reason']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->save($webhooks);
|
||||
}
|
||||
|
||||
private function load(): array
|
||||
{
|
||||
if ($this->webhooksCache !== null) {
|
||||
return $this->webhooksCache;
|
||||
}
|
||||
|
||||
$file = YamlFile::instance($this->storagePath . '/webhooks.yaml');
|
||||
$content = $file->content();
|
||||
$this->webhooksCache = $content['webhooks'] ?? [];
|
||||
|
||||
return $this->webhooksCache;
|
||||
}
|
||||
|
||||
private function save(array $webhooks): void
|
||||
{
|
||||
$file = YamlFile::instance($this->storagePath . '/webhooks.yaml');
|
||||
$file->content(['webhooks' => array_values($webhooks)]);
|
||||
$file->save();
|
||||
$this->webhooksCache = array_values($webhooks);
|
||||
}
|
||||
|
||||
private function pruneDeliveries(string $webhookId, int $keep): void
|
||||
{
|
||||
$dir = $this->deliveryPath . '/' . $webhookId;
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = glob($dir . '/*.yaml');
|
||||
usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a));
|
||||
|
||||
$toDelete = array_slice($files, $keep);
|
||||
foreach ($toDelete as $file) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user