Add workspace webhooks and drop the unused automation webhook node (#326)

* Add workspace webhooks and drop the unused automation webhook node.

Give workspaces HMAC-signed outgoing webhooks for the post lifecycle, with retry, auto-pause, replay, and live logs, and keep HTTP Request as the only outbound automation node.

* Tighten webhook controller and validation after review.

Drop the redundant workspace redirects, prune logs without counting, and validate events/status with Rule::enum.

* Move leftover webhook UI copy behind i18n.

HTTP status phrases, delete-cancel, and validation attribute names were still English literals.

* Build the webhook-paused email through Maizzle.

The hand-written Blade skipped the shared layout, header, and footer used by the other mail templates.

* Cover real webhook dispatch paths and restyle the webhook pages.

* Ask for the shared delete keyword when confirming a webhook delete.

The endpoint URL is a poor confirm string; posts and assets already use the common "delete" keyword.

* Fix webhook review blockers so CI can go green.

Drop leftover French automation keys, stop mutating Inertia log props, and show delivered_at instead of created_at.

* Close the remaining webhook review gaps.

Keep Echo log updates across infinite scroll, align the channel with the policy, persist log ids across retries, and fail unknown automation nodes without throwing.

* Stop webhook delivery after disable and record last sent only on success.

Queued jobs now skip paused or disabled endpoints unless the user replays, and changing the URL re-pings it first.

* Limit webhooks to owners and admins, and encrypt signing secrets.

Members can no longer create or inspect outgoing integrations, and secrets stay encrypted at rest.

* Cover webhook secret hiding, skip-ping, and failed-delivery edges.

* Send the full post on webhooks after labels and platforms are saved.

* Fix webhook payloads for integer media ids and type webhook status.

* Split the webhook show page into focused components.

* Reset live webhook logs when switching endpoints.

* Keep the newest webhook logs at the top after live merges.

* Cast media item ids to string without the extra scalar check.

* Add post.unscheduled webhooks and put the log id on the envelope.

Unscheduling is now a first-class event, and receivers can send the delivery id back so we can find the matching log.

* Translate webhook event names in the UI.

* Make the webhook show page full-width and stop stacking flash toasts.

* Translate remaining webhook UI copy in every locale.

* Sign webhook pings and drop author email from the payload.

* Send signed webhook tests after create instead of pinging on save.

Create and update only block private URLs so the receiver can copy the secret first. The show page then sends a signed webhook.test with an object data envelope.

* Polish webhook test UX and always mint the dispatch log id in the job.

Keep send-test in the actions menu (its own group) and drop the leftover constructor param so retries reuse the serialized id instead of a caller-supplied one.
This commit is contained in:
Paulo Castellano 2026-09-04 09:43:29 -03:00 committed by GitHub
parent ca497e1f3c
commit 58d8e066b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
144 changed files with 8166 additions and 1020 deletions

View file

@ -35,7 +35,7 @@ ## What you get
| 📅  **One calendar, every network** | Plan a month at a glance, drag any post to a new slot, and publish natively to 12 platforms. No redirects, no "finish in the mobile app." | | 📅  **One calendar, every network** | Plan a month at a glance, drag any post to a new slot, and publish natively to 12 platforms. No redirects, no "finish in the mobile app." |
| ✨  **An AI copilot that knows your brand** | Captions, hooks, full drafts, and multi-slide carousels in your tone, voice, and colors. It reads your brand profile on every generation. | | ✨  **An AI copilot that knows your brand** | Captions, hooks, full drafts, and multi-slide carousels in your tone, voice, and colors. It reads your brand profile on every generation. |
| 🤖  **Built for AI agents** | A first-class MCP server and REST API. Claude, Cursor, ChatGPT, or your own scripts can draft, schedule, and publish for you. | | 🤖  **Built for AI agents** | A first-class MCP server and REST API. Claude, Cursor, ChatGPT, or your own scripts can draft, schedule, and publish for you. |
| ⚙️  **Automations that run themselves** | A visual workflow builder: triggers, conditions, RSS, webhooks, and AI generation, all server-side. Set it once, let it post. | | ⚙️  **Automations that run themselves** | A visual workflow builder: triggers, conditions, RSS, HTTP requests, and AI generation, all server-side. Set it once, let it post. |
| 🗂️  **Made for many clients** | Workspaces, roles, and approval flows so an agency or freelancer can run a roster of brands without the spreadsheets. | | 🗂️  **Made for many clients** | Workspaces, roles, and approval flows so an agency or freelancer can run a roster of brands without the spreadsheets. |
## Features ## Features
@ -47,7 +47,7 @@ ## Features
| **AI generate & review** | Draft from a prompt, get inline feedback before you publish. | | **AI generate & review** | Draft from a prompt, get inline feedback before you publish. |
| **AI carousel builder** | Prompt to a multi-slide carousel with images, on-brand. | | **AI carousel builder** | Prompt to a multi-slide carousel with images, on-brand. |
| **Brand profile** | Tone, voice, language, and colors applied to every AI call. | | **Brand profile** | Tone, voice, language, and colors applied to every AI call. |
| **Automations** | Schedule / RSS triggers, conditions, publish steps, and webhooks. | | **Automations** | Schedule / RSS triggers, conditions, publish steps, and HTTP requests. |
| **Asset library** | Reusable workspace media, plus Unsplash and Giphy search built in. | | **Asset library** | Reusable workspace media, plus Unsplash and Giphy search built in. |
| **Signatures & labels** | Reusable hashtag and CTA blocks, color-coded post tags. | | **Signatures & labels** | Reusable hashtag and CTA blocks, color-coded post tags. |
| **Team collaboration** | Owner / Admin / Member roles, comments with @mentions on drafts. | | **Team collaboration** | Owner / Admin / Member roles, comments with @mentions on drafts. |

View file

@ -24,7 +24,7 @@
* - When the fetch returns N new items, the current run takes item[0]; the * - When the fetch returns N new items, the current run takes item[0]; the
* remaining N-1 items are spawned as sibling runs that resume at the node * remaining N-1 items are spawned as sibling runs that resume at the node
* immediately after this Fetch (with `context.fetched` already populated), * immediately after this Fetch (with `context.fetched` already populated),
* so each item ends up generating its own Post / Webhook / etc. * so each item ends up generating its own Post / etc.
* - When the feed yields no new items, the result short-circuits via the * - When the feed yields no new items, the result short-circuits via the
* `no_items` handle; if the user hasn't wired anything to it, the run * `no_items` handle; if the user hasn't wired anything to it, the run
* completes silently (handled by AdvanceAutomationRun's default branch). * completes silently (handled by AdvanceAutomationRun's default branch).

View file

@ -1,101 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Automation\Node;
use App\DataTransferObjects\Automation\NodeRunResult;
use App\Enums\Automation\HttpMethod;
use App\Models\AutomationRun;
use App\Services\Automation\ExpressionResolver;
use App\Services\Brand\SafeHttpFetcher;
use Illuminate\Support\Facades\Http;
use RuntimeException;
use Throwable;
class RunWebhookNode
{
public function __construct(
private ExpressionResolver $resolver,
private SafeHttpFetcher $safeHttp,
) {}
public function __invoke(AutomationRun $run, array $config): NodeRunResult
{
$context = $run->resolverContext();
$url = $this->resolver->resolve((string) data_get($config, 'url', ''), $context);
$method = strtoupper((string) data_get($config, 'method', HttpMethod::Post->value));
if ($url === '') {
return NodeRunResult::failed(__('automations.errors.webhook_missing_url'), [
'reason' => 'missing_url',
]);
}
try {
$this->safeHttp->guardAgainstSsrf($url);
} catch (RuntimeException) {
return NodeRunResult::failed(__('automations.errors.url_not_allowed'), [
'reason' => 'url_not_allowed',
'url' => $url,
]);
}
$headers = [];
foreach ($config['headers'] ?? [] as $k => $v) {
$headers[$k] = $this->resolver->resolve((string) $v, $context);
}
// Parse the template as JSON FIRST, then resolve placeholders in its
// string leaves — so a value containing `"`/`&`/newlines can't corrupt
// the JSON (the final json_encode escapes it).
$template = (string) data_get($config, 'payload_template', '{}');
$trimmedTemplate = trim($template);
if ($trimmedTemplate === '' || $trimmedTemplate === 'null') {
$payload = [];
} else {
$decodedTemplate = json_decode($template, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return NodeRunResult::failed(__('automations.errors.webhook_invalid_payload_json'), [
'reason' => 'invalid_payload_json',
]);
}
$payload = $this->resolver->resolveStructured($decodedTemplate ?? [], $context);
}
if ($run->is_dry_run) {
return NodeRunResult::completed(output: [
'webhook' => ['method' => $method, 'url' => $url, 'dry_run' => true],
]);
}
try {
$response = Http::withHeaders($headers)
->withUserAgent(config('trypost.user_agent'))
->withOptions(['allow_redirects' => false])
->send($method, $url, ['json' => $payload]);
} catch (Throwable $e) {
return NodeRunResult::failed(__('automations.errors.webhook_request_failed'), [
'reason' => 'request_failed',
'message' => $e->getMessage(),
]);
}
if ($response->serverError()) {
return NodeRunResult::failed(__('automations.errors.webhook_server_error'), [
'status' => $response->status(),
'body' => substr($response->body(), 0, 500),
]);
}
return NodeRunResult::completed(output: [
'webhook' => [
'status' => $response->status(),
'body' => substr($response->body(), 0, 500),
],
]);
}
}

View file

@ -25,26 +25,26 @@ public static function execute(Workspace $workspace, Post $post, array $data): a
return ['post' => $post, 'action' => PostAction::Finalized]; return ['post' => $post, 'action' => PostAction::Finalized];
} }
$scheduledAt = $post->scheduled_at; return DB::transaction(function () use ($post, $data): array {
if (data_get($data, 'scheduled_at')) { $scheduledAt = $post->scheduled_at;
$scheduledAt = Carbon::parse(data_get($data, 'scheduled_at'))->utc(); if (data_get($data, 'scheduled_at')) {
} $scheduledAt = Carbon::parse(data_get($data, 'scheduled_at'))->utc();
}
$status = data_get($data, 'status', $post->status); $status = data_get($data, 'status', $post->status);
$post->update([ $post->update([
'content' => data_get($data, 'content', $post->content), 'content' => data_get($data, 'content', $post->content),
'media' => data_get($data, 'media', $post->media), 'media' => data_get($data, 'media', $post->media),
'status' => $status === PostStatus::Publishing->value ? PostStatus::Publishing : $status, 'status' => $status === PostStatus::Publishing->value ? PostStatus::Publishing : $status,
'scheduled_at' => $scheduledAt, 'scheduled_at' => $scheduledAt,
]); ]);
if (Arr::has($data, 'label_ids')) { if (Arr::has($data, 'label_ids')) {
$post->labels()->sync(data_get($data, 'label_ids', [])); $post->labels()->sync(data_get($data, 'label_ids', []));
} }
if (Arr::has($data, 'platforms')) { if (Arr::has($data, 'platforms')) {
DB::transaction(function () use ($post, $data) {
$post->postPlatforms()->update(['enabled' => false]); $post->postPlatforms()->update(['enabled' => false]);
foreach (data_get($data, 'platforms', []) as $platformData) { foreach (data_get($data, 'platforms', []) as $platformData) {
@ -69,20 +69,20 @@ public static function execute(Workspace $workspace, Post $post, array $data): a
->where('id', data_get($platformData, 'id')) ->where('id', data_get($platformData, 'id'))
->update($updateData); ->update($updateData);
} }
}); }
}
if ($status === PostStatus::Publishing->value) { if ($status === PostStatus::Publishing->value) {
$post->update(['scheduled_at' => now()]); $post->update(['scheduled_at' => now()]);
PublishPost::dispatch($post); PublishPost::dispatch($post)->afterCommit();
return ['post' => $post, 'action' => PostAction::Publishing]; return ['post' => $post, 'action' => PostAction::Publishing];
} }
if ($status === PostStatus::Scheduled->value) { if ($status === PostStatus::Scheduled->value) {
return ['post' => $post, 'action' => PostAction::Scheduled]; return ['post' => $post, 'action' => PostAction::Scheduled];
} }
return ['post' => $post, 'action' => null]; return ['post' => $post, 'action' => null];
});
} }
} }

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Broadcasting;
use App\Models\User;
use App\Models\Webhook;
class WebhookLogChannel
{
public function join(User $user, Webhook $webhook): bool
{
return $user->can('view', $webhook);
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\WebhookLog;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
#[Signature('app:prune-webhook-logs')]
#[Description('Delete webhook logs older than 7 days')]
class PruneWebhookLogs extends Command
{
public function handle(): int
{
WebhookLog::query()
->where('created_at', '<', now()->subDays(7))
->delete();
return self::SUCCESS;
}
}

View file

@ -122,7 +122,7 @@ public static function fromArray(array $data): self
$meta = data_get($data, 'meta'); $meta = data_get($data, 'meta');
return new self( return new self(
id: data_get($data, 'id', ''), id: (string) data_get($data, 'id', ''),
path: $path, path: $path,
url: data_get($data, 'url', ''), url: data_get($data, 'url', ''),
mime_type: $mimeType, mime_type: $mimeType,

View file

@ -5,7 +5,7 @@
namespace App\Enums\Automation; namespace App\Enums\Automation;
/** /**
* HTTP verbs available to the HTTP Request and Webhook nodes. Mirrors the * HTTP verbs available to the HTTP Request node. Mirrors the
* frontend HttpMethod const (resources/js/types/automation/http-method.ts). * frontend HttpMethod const (resources/js/types/automation/http-method.ts).
*/ */
enum HttpMethod: string enum HttpMethod: string

View file

@ -11,7 +11,6 @@ enum Type: string
case Delay = 'delay'; case Delay = 'delay';
case Condition = 'condition'; case Condition = 'condition';
case Publish = 'publish'; case Publish = 'publish';
case Webhook = 'webhook';
case End = 'end'; case End = 'end';
case FetchRss = 'fetch_rss'; case FetchRss = 'fetch_rss';
case HttpRequest = 'http_request'; case HttpRequest = 'http_request';

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Enums\Webhook;
use App\Enums\Post\Status as PostStatus;
enum EventType: string
{
case PostCreated = 'post.created';
case PostScheduled = 'post.scheduled';
case PostUnscheduled = 'post.unscheduled';
case PostPublished = 'post.published';
case PostPartiallyPublished = 'post.partially_published';
case PostFailed = 'post.failed';
case PostDeleted = 'post.deleted';
public static function fromPostStatus(PostStatus $status, ?PostStatus $previous = null): ?self
{
return match ($status) {
PostStatus::Scheduled => self::PostScheduled,
PostStatus::Published => self::PostPublished,
PostStatus::PartiallyPublished => self::PostPartiallyPublished,
PostStatus::Failed => self::PostFailed,
PostStatus::Draft => $previous === PostStatus::Scheduled ? self::PostUnscheduled : null,
default => null,
};
}
}

View file

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Enums\Webhook;
enum Status: string
{
case Enabled = 'enabled';
case Disabled = 'disabled';
case Paused = 'paused';
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Events;
use App\Enums\Post\Status as PostStatus;
use App\Models\Post;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PostStatusChanged
{
use Dispatchable, SerializesModels;
public function __construct(public Post $post, public ?PostStatus $previousStatus = null) {}
}

View file

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Events\Webhook;
use App\Models\WebhookLog;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class LogUpdated implements ShouldBroadcast
{
use Dispatchable, SerializesModels;
public int $tries = 3;
/** @var array<int, int> */
public array $backoff = [5, 10];
public function __construct(
public WebhookLog $log,
) {}
/**
* @return array<int, PrivateChannel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel("webhook.{$this->log->webhook_id}.logs"),
];
}
public function broadcastAs(): string
{
return 'webhook.log.updated';
}
public function broadcastQueue(): string
{
return 'broadcasts';
}
/**
* @return array<string, mixed>
*/
public function broadcastWith(): array
{
return [
'id' => $this->log->id,
'event_type' => $this->log->event_type,
'payload' => $this->log->payload,
'response_status' => $this->log->response_status,
'response_body' => $this->log->response_body,
'delivered_at' => $this->log->delivered_at?->toIso8601String(),
'failed_at' => $this->log->failed_at?->toIso8601String(),
'attempts' => $this->log->attempts,
'created_at' => $this->log->created_at->toIso8601String(),
];
}
}

View file

@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\App;
use App\Enums\Webhook\Status;
use App\Http\Requests\App\Webhook\StoreWebhookRequest;
use App\Http\Requests\App\Webhook\UpdateWebhookRequest;
use App\Jobs\DispatchWebhook;
use App\Models\Webhook;
use App\Models\WebhookLog;
use App\Services\WebhookService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use RuntimeException;
class WebhookController extends Controller
{
public function index(Request $request): Response
{
$workspace = $request->user()->currentWorkspace;
$this->authorize('viewAny', Webhook::class);
$webhooks = Webhook::query()
->where('workspace_id', $workspace->id)
->orderByDesc('created_at')
->get();
return Inertia::render('webhooks/Index', [
'webhooks' => $webhooks,
]);
}
public function show(Webhook $webhook): Response
{
$this->authorize('view', $webhook);
return Inertia::render('webhooks/Show', [
'webhook' => $webhook->makeVisible('signing_secret'),
'logs' => Inertia::scroll(
fn () => $webhook->logs()->orderByDesc('created_at')->paginate((int) config('app.pagination.default')),
),
]);
}
public function store(StoreWebhookRequest $request, WebhookService $webhookService): RedirectResponse
{
$workspace = $request->user()->currentWorkspace;
$this->authorize('create', Webhook::class);
$validated = $request->validated();
$endpoint = data_get($validated, 'endpoint');
if ($error = $this->endpointError($webhookService, $endpoint)) {
return $error;
}
$webhook = Webhook::query()->create([
'workspace_id' => $workspace->id,
'endpoint' => $endpoint,
'events' => data_get($validated, 'events'),
'status' => Status::Enabled,
'signing_secret' => Webhook::generateSigningSecret(),
]);
session()->flash('flash.banner', __('webhooks.flash.created'));
session()->flash('flash.bannerStyle', 'success');
return redirect()->route('app.webhooks.show', $webhook);
}
public function update(UpdateWebhookRequest $request, Webhook $webhook, WebhookService $webhookService): RedirectResponse
{
$this->authorize('update', $webhook);
$validated = $request->validated();
$endpoint = data_get($validated, 'endpoint');
if ($endpoint !== $webhook->endpoint && ($error = $this->endpointError($webhookService, $endpoint))) {
return $error;
}
if (data_get($validated, 'status') === Status::Enabled->value) {
$validated['consecutive_failures'] = 0;
$validated['paused_at'] = null;
}
$webhook->update($validated);
session()->flash('flash.banner', __('webhooks.flash.updated'));
session()->flash('flash.bannerStyle', 'success');
return back();
}
public function sendTest(Webhook $webhook, WebhookService $webhookService): RedirectResponse
{
$this->authorize('update', $webhook);
try {
$webhookService->ping($webhook->endpoint, $webhook->signing_secret);
} catch (RuntimeException $e) {
session()->flash('flash.banner', $e->getMessage());
session()->flash('flash.bannerStyle', 'danger');
return back();
}
session()->flash('flash.banner', __('webhooks.flash.tested'));
session()->flash('flash.bannerStyle', 'success');
return back();
}
public function rotateSecret(Webhook $webhook): RedirectResponse
{
$this->authorize('update', $webhook);
$webhook->update([
'signing_secret' => Webhook::generateSigningSecret(),
]);
session()->flash('flash.banner', __('webhooks.flash.secret_rotated'));
session()->flash('flash.bannerStyle', 'success');
return back();
}
public function replay(Webhook $webhook, WebhookLog $webhookLog): RedirectResponse
{
$this->authorize('replay', [$webhookLog, $webhook]);
DispatchWebhook::dispatch(
$webhook,
$webhookLog->event_type,
data_get($webhookLog->payload, 'data') ?? [],
force: true,
);
session()->flash('flash.banner', __('webhooks.flash.replayed'));
session()->flash('flash.bannerStyle', 'success');
return back();
}
public function destroy(Webhook $webhook): RedirectResponse
{
$this->authorize('delete', $webhook);
$webhook->delete();
session()->flash('flash.banner', __('webhooks.flash.deleted'));
session()->flash('flash.bannerStyle', 'success');
return redirect()->route('app.webhooks.index');
}
private function endpointError(WebhookService $webhookService, mixed $endpoint): ?RedirectResponse
{
if (! is_string($endpoint)) {
return null;
}
try {
$webhookService->assertEndpointAllowed($endpoint);
} catch (RuntimeException $e) {
return back()->withErrors([
'endpoint' => $e->getMessage(),
]);
}
return null;
}
}

View file

@ -70,9 +70,8 @@ public function rules(): array
/** /**
* Block saving a node whose config can't run: a Generate node whose image * Block saving a node whose config can't run: a Generate node whose image
* count doesn't fit a selected account's content-type, or a Webhook node * count doesn't fit a selected account's content-type. Each issue is keyed
* whose payload template isn't valid JSON. Each issue is keyed to the field * to the field the frontend surfaces it under.
* the frontend surfaces it under (mirrors the inline frontend validation).
*/ */
public function withValidator(Validator $validator): void public function withValidator(Validator $validator): void
{ {
@ -172,13 +171,6 @@ private function dataRulesForNodeType(?string $type, int $i): array
'mode' => ['required', Rule::in(array_column(PublishMode::cases(), 'value'))], 'mode' => ['required', Rule::in(array_column(PublishMode::cases(), 'value'))],
'scheduled_offset' => ['required_if:nodes.'.$i.'.data.mode,'.PublishMode::Scheduled->value, 'integer', 'min:0'], 'scheduled_offset' => ['required_if:nodes.'.$i.'.data.mode,'.PublishMode::Scheduled->value, 'integer', 'min:0'],
], ],
NodeType::Webhook->value => [
'url' => ['required', new ResolvableUrl],
'method' => ['required', Rule::in(array_column(HttpMethod::cases(), 'value'))],
'payload_template' => ['nullable', 'string'],
'headers' => ['nullable', 'array'],
'headers.*' => ['string'],
],
NodeType::End->value => [ NodeType::End->value => [
'reason' => ['nullable', 'string'], 'reason' => ['nullable', 'string'],
], ],

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\App\Webhook;
use App\Enums\Webhook\EventType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreWebhookRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'endpoint' => ['required', 'url', 'max:255'],
'events' => ['required', 'array', 'min:1'],
'events.*' => ['string', Rule::enum(EventType::class)],
];
}
/**
* @return array<string, string>
*/
public function attributes(): array
{
return [
'endpoint' => __('webhooks.create.endpoint'),
'events' => __('webhooks.create.events'),
'events.*' => __('webhooks.create.events'),
];
}
}

View file

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\App\Webhook;
use App\Enums\Webhook\EventType;
use App\Enums\Webhook\Status;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateWebhookRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'endpoint' => ['sometimes', 'url', 'max:255'],
'events' => ['sometimes', 'array', 'min:1'],
'events.*' => ['string', Rule::enum(EventType::class)],
'status' => ['sometimes', 'string', Rule::enum(Status::class)->only([Status::Enabled, Status::Disabled])],
];
}
/**
* @return array<string, string>
*/
public function attributes(): array
{
return [
'endpoint' => __('webhooks.create.endpoint'),
'events' => __('webhooks.create.events'),
'events.*' => __('webhooks.create.events'),
'status' => __('webhooks.table.status'),
];
}
}

View file

@ -11,7 +11,6 @@
use App\Actions\Automation\Node\RunGenerateNode; use App\Actions\Automation\Node\RunGenerateNode;
use App\Actions\Automation\Node\RunHttpRequestNode; use App\Actions\Automation\Node\RunHttpRequestNode;
use App\Actions\Automation\Node\RunPublishNode; use App\Actions\Automation\Node\RunPublishNode;
use App\Actions\Automation\Node\RunWebhookNode;
use App\Actions\Automation\Run\AdvanceAutomationRun; use App\Actions\Automation\Run\AdvanceAutomationRun;
use App\DataTransferObjects\Automation\NodeRunResult; use App\DataTransferObjects\Automation\NodeRunResult;
use App\Enums\Automation\Node\Type as NodeType; use App\Enums\Automation\Node\Type as NodeType;
@ -65,7 +64,17 @@ public function handle(AdvanceAutomationRun $advance): void
return; return;
} }
$nodeType = NodeType::from($node['type']); $nodeType = NodeType::tryFrom((string) data_get($node, 'type', ''));
if ($nodeType === null) {
$this->run->update([
'status' => RunStatus::Failed,
'error' => ['message' => __('automations.errors.node_no_longer_exists', ['node_id' => $this->nodeId])],
'finished_at' => now(),
]);
return;
}
$this->run->update([ $this->run->update([
'status' => RunStatus::Running, 'status' => RunStatus::Running,
@ -143,7 +152,6 @@ private function executeNode(NodeType $type, array $config): NodeRunResult
NodeType::Delay => app(RunDelayNode::class), NodeType::Delay => app(RunDelayNode::class),
NodeType::Condition => app(RunConditionNode::class), NodeType::Condition => app(RunConditionNode::class),
NodeType::Publish => app(RunPublishNode::class), NodeType::Publish => app(RunPublishNode::class),
NodeType::Webhook => app(RunWebhookNode::class),
NodeType::End => app(RunEndNode::class), NodeType::End => app(RunEndNode::class),
NodeType::FetchRss => app(RunFetchRssNode::class), NodeType::FetchRss => app(RunFetchRssNode::class),
NodeType::HttpRequest => app(RunHttpRequestNode::class), NodeType::HttpRequest => app(RunHttpRequestNode::class),

View file

@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Enums\Webhook\Status;
use App\Events\Webhook\LogUpdated;
use App\Mail\WebhookPausedMail;
use App\Models\Webhook;
use App\Models\WebhookLog;
use App\Services\Brand\SafeHttpFetcher;
use App\Services\WebhookService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use RuntimeException;
use Throwable;
class DispatchWebhook implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public bool $deleteWhenMissingModels = true;
public int $tries = 3;
public int $timeout = 30;
public int $backoff = 60;
public string $logId;
/**
* @param array<string, mixed> $payload
*/
public function __construct(
public Webhook $webhook,
public string $eventType,
public array $payload,
public bool $force = false,
) {
$this->onQueue('webhooks');
$this->logId = (string) Str::uuid();
}
public function handle(SafeHttpFetcher $safeHttp, WebhookService $webhooks): void
{
$this->webhook->refresh();
if (! $this->force && $this->webhook->status !== Status::Enabled) {
return;
}
$body = [
'id' => $this->logId,
'type' => $this->eventType,
'data' => $this->payload,
'created_at' => now()->toIso8601String(),
];
$signature = $webhooks->signature($body, $this->webhook->signing_secret);
$log = WebhookLog::query()->find($this->logId);
if ($log) {
$log->update([
'payload' => $body,
'attempts' => $this->attempts(),
'failed_at' => null,
'response_status' => null,
'response_body' => null,
'delivered_at' => null,
]);
} else {
$log = new WebhookLog([
'webhook_id' => $this->webhook->id,
'event_type' => $this->eventType,
'payload' => $body,
'attempts' => $this->attempts(),
]);
$log->id = $this->logId;
$log->save();
}
try {
$safeHttp->guardAgainstSsrf($this->webhook->endpoint);
} catch (RuntimeException $e) {
$log->update([
'failed_at' => now(),
'response_body' => $e->getMessage(),
]);
$log->refresh();
LogUpdated::dispatch($log);
throw $e;
}
try {
$response = Http::timeout(10)
->withUserAgent(config('trypost.user_agent'))
->withOptions(['allow_redirects' => false])
->asJson()
->withHeaders([
'X-Webhook-Signature' => $signature,
])
->post($this->webhook->endpoint, $body);
$responseBody = substr($response->body(), 0, 2000);
if ($response->successful()) {
$log->update([
'response_status' => $response->status(),
'response_body' => $responseBody,
'delivered_at' => now(),
]);
$this->webhook->update(['last_sent_at' => now()]);
$this->webhook->resetConsecutiveFailures();
$log->refresh();
LogUpdated::dispatch($log);
return;
}
$log->update([
'response_status' => $response->status(),
'response_body' => $responseBody,
'failed_at' => now(),
]);
$log->refresh();
LogUpdated::dispatch($log);
throw new RuntimeException("Webhook delivery failed with status: {$response->status()}");
} catch (ConnectionException $e) {
$log->update([
'failed_at' => now(),
'response_body' => $e->getMessage(),
]);
$log->refresh();
LogUpdated::dispatch($log);
throw $e;
}
}
public function failed(?Throwable $exception): void
{
$this->webhook->refresh();
if ($this->webhook->status !== Status::Enabled) {
return;
}
$this->webhook->increment('consecutive_failures');
$this->webhook->refresh();
if ($this->webhook->consecutive_failures >= 5) {
$this->webhook->pause();
$owner = $this->webhook->workspace?->account?->owner;
if ($owner?->email) {
Mail::to($owner->email)->send(new WebhookPausedMail($this->webhook));
}
}
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Listeners\Webhook;
use App\Enums\Webhook\EventType;
use App\Events\PostCreated;
use App\Services\WebhookService;
class SendPostCreatedWebhook
{
public function __construct(private WebhookService $webhooks) {}
public function handle(PostCreated $event): void
{
$post = $event->post;
$workspace = $post->workspace;
if ($workspace === null) {
return;
}
$this->webhooks->dispatch($workspace, EventType::PostCreated, $this->webhooks->postPayload($post));
}
}

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Listeners\Webhook;
use App\Enums\Webhook\EventType;
use App\Events\PostDeleted;
use App\Models\Workspace;
use App\Services\WebhookService;
class SendPostDeletedWebhook
{
public function __construct(private WebhookService $webhooks) {}
public function handle(PostDeleted $event): void
{
$workspace = Workspace::query()->find($event->workspaceId);
if ($workspace === null) {
return;
}
$this->webhooks->dispatch($workspace, EventType::PostDeleted, [
'id' => $event->postId,
'workspace_id' => $event->workspaceId,
]);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Listeners\Webhook;
use App\Enums\Webhook\EventType;
use App\Events\PostStatusChanged;
use App\Services\WebhookService;
class SendPostStatusWebhook
{
public function __construct(private WebhookService $webhooks) {}
public function handle(PostStatusChanged $event): void
{
$webhookEvent = EventType::fromPostStatus($event->post->status, $event->previousStatus);
if ($webhookEvent === null) {
return;
}
$workspace = $event->post->workspace;
if ($workspace === null) {
return;
}
$this->webhooks->dispatch($workspace, $webhookEvent, $this->webhooks->postPayload($event->post));
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Mail;
use App\Models\Webhook;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class WebhookPausedMail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(public Webhook $webhook) {}
public function envelope(): Envelope
{
return new Envelope(
subject: __('webhooks.mail.paused_subject', ['endpoint' => $this->webhook->endpoint]),
);
}
public function content(): Content
{
return new Content(
view: 'mail.webhook-paused',
with: [
'title' => __('webhooks.mail.paused_title'),
'previewText' => __('webhooks.mail.paused_preview'),
'body' => __('webhooks.mail.paused_body', ['endpoint' => $this->webhook->endpoint]),
'buttonText' => __('webhooks.mail.paused_cta'),
'url' => route('app.webhooks.show', $this->webhook),
],
);
}
}

90
app/Models/Webhook.php Normal file
View file

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Enums\Webhook\Status;
use Database\Factories\WebhookFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
class Webhook extends Model
{
/** @use HasFactory<WebhookFactory> */
use HasFactory, HasUuids;
protected $fillable = [
'workspace_id',
'endpoint',
'events',
'status',
'signing_secret',
'consecutive_failures',
'paused_at',
'last_sent_at',
];
protected $hidden = [
'signing_secret',
];
protected $attributes = [
'consecutive_failures' => 0,
];
protected function casts(): array
{
return [
'events' => 'array',
'status' => Status::class,
'signing_secret' => 'encrypted',
'paused_at' => 'datetime',
'last_sent_at' => 'datetime',
];
}
public static function generateSigningSecret(): string
{
return 'whsec_'.Str::random(32);
}
public function pause(): void
{
$this->update([
'status' => Status::Paused,
'paused_at' => now(),
]);
}
public function resetConsecutiveFailures(): void
{
if ($this->consecutive_failures > 0) {
$this->update(['consecutive_failures' => 0]);
}
}
/**
* @param Builder<self> $query
* @return Builder<self>
*/
public function scopeEnabled(Builder $query): Builder
{
return $query->where('status', Status::Enabled);
}
public function workspace(): BelongsTo
{
return $this->belongsTo(Workspace::class);
}
public function logs(): HasMany
{
return $this->hasMany(WebhookLog::class);
}
}

44
app/Models/WebhookLog.php Normal file
View file

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Database\Factories\WebhookLogFactory;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class WebhookLog extends Model
{
/** @use HasFactory<WebhookLogFactory> */
use HasFactory, HasUuids;
protected $fillable = [
'webhook_id',
'event_type',
'payload',
'response_status',
'response_body',
'delivered_at',
'failed_at',
'attempts',
];
protected function casts(): array
{
return [
'payload' => 'array',
'response_status' => 'integer',
'delivered_at' => 'datetime',
'failed_at' => 'datetime',
'attempts' => 'integer',
];
}
public function webhook(): BelongsTo
{
return $this->belongsTo(Webhook::class);
}
}

View file

@ -92,6 +92,11 @@ public function labels(): HasMany
return $this->hasMany(WorkspaceLabel::class); return $this->hasMany(WorkspaceLabel::class);
} }
public function webhooks(): HasMany
{
return $this->hasMany(Webhook::class);
}
/** /**
* Get invites for this workspace (invites from the same account that include this workspace). * Get invites for this workspace (invites from the same account that include this workspace).
* *

View file

@ -8,6 +8,7 @@
use App\Enums\Post\Status as PostStatus; use App\Enums\Post\Status as PostStatus;
use App\Events\OnboardingStatusUpdated; use App\Events\OnboardingStatusUpdated;
use App\Events\PostCreated; use App\Events\PostCreated;
use App\Events\PostStatusChanged;
use App\Jobs\Automation\DispatchPostTriggerAutomationsJob; use App\Jobs\Automation\DispatchPostTriggerAutomationsJob;
use App\Models\Account; use App\Models\Account;
use App\Models\Post; use App\Models\Post;
@ -43,6 +44,25 @@ public function saved(Post $post): void
if ($triggerType !== null) { if ($triggerType !== null) {
DispatchPostTriggerAutomationsJob::dispatch($post, $triggerType)->afterCommit(); DispatchPostTriggerAutomationsJob::dispatch($post, $triggerType)->afterCommit();
} }
$previousStatus = $this->previousStatus($post);
DB::afterCommit(fn () => PostStatusChanged::dispatch($post, $previousStatus));
}
private function previousStatus(Post $post): ?PostStatus
{
$previous = $post->getRawOriginal('status');
if ($previous instanceof PostStatus) {
return $previous;
}
if (is_string($previous)) {
return PostStatus::tryFrom($previous);
}
return null;
} }
/** /**

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Models\User;
use App\Models\Webhook;
use App\Models\WebhookLog;
class WebhookLogPolicy
{
public function replay(User $user, WebhookLog $webhookLog, Webhook $webhook): bool
{
return $webhookLog->webhook_id === $webhook->id
&& $webhook->workspace_id === $user->current_workspace_id
&& $user->can('manageWebhooks', $user->currentWorkspace);
}
}

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Models\User;
use App\Models\Webhook;
class WebhookPolicy
{
public function viewAny(User $user): bool
{
return $user->currentWorkspace !== null
&& $user->can('manageWebhooks', $user->currentWorkspace);
}
public function view(User $user, Webhook $webhook): bool
{
return $webhook->workspace_id === $user->current_workspace_id
&& $user->can('manageWebhooks', $user->currentWorkspace);
}
public function create(User $user): bool
{
return $this->viewAny($user);
}
public function update(User $user, Webhook $webhook): bool
{
return $this->view($user, $webhook);
}
public function delete(User $user, Webhook $webhook): bool
{
return $this->view($user, $webhook);
}
}

View file

@ -56,6 +56,11 @@ public function manageAccounts(User $user, Workspace $workspace): bool
return $this->isOwnerOrWorkspaceAdmin($user, $workspace); return $this->isOwnerOrWorkspaceAdmin($user, $workspace);
} }
public function manageWebhooks(User $user, Workspace $workspace): bool
{
return $this->isOwnerOrWorkspaceAdmin($user, $workspace);
}
public function createPost(User $user, Workspace $workspace): bool public function createPost(User $user, Workspace $workspace): bool
{ {
if ($this->isOwner($user, $workspace)) { if ($this->isOwner($user, $workspace)) {

View file

@ -25,6 +25,8 @@
use App\Models\Subscription; use App\Models\Subscription;
use App\Models\SubscriptionItem; use App\Models\SubscriptionItem;
use App\Models\User; use App\Models\User;
use App\Models\Webhook;
use App\Models\WebhookLog;
use App\Models\Workspace; use App\Models\Workspace;
use App\Models\WorkspaceInvite; use App\Models\WorkspaceInvite;
use App\Models\WorkspaceLabel; use App\Models\WorkspaceLabel;
@ -115,6 +117,8 @@ protected function configureMorphMap(): void
'subscription' => Subscription::class, 'subscription' => Subscription::class,
'subscriptionItem' => SubscriptionItem::class, 'subscriptionItem' => SubscriptionItem::class,
'user' => User::class, 'user' => User::class,
'webhook' => Webhook::class,
'webhookLog' => WebhookLog::class,
'workspace' => Workspace::class, 'workspace' => Workspace::class,
'workspaceInvite' => WorkspaceInvite::class, 'workspaceInvite' => WorkspaceInvite::class,
'workspaceLabel' => WorkspaceLabel::class, 'workspaceLabel' => WorkspaceLabel::class,

View file

@ -17,7 +17,6 @@ final class AutomationConfigValidator
{ {
public function __construct( public function __construct(
private GenerateNodeValidator $generateValidator, private GenerateNodeValidator $generateValidator,
private WebhookNodeValidator $webhookValidator,
) {} ) {}
/** /**
@ -35,7 +34,6 @@ public function issues(array $nodes): array
[$field, $message] = match (data_get($node, 'type')) { [$field, $message] = match (data_get($node, 'type')) {
NodeType::Generate->value => ['accounts', $this->generateValidator->issueFor($config)], NodeType::Generate->value => ['accounts', $this->generateValidator->issueFor($config)],
NodeType::Webhook->value => ['payload_template', $this->webhookValidator->issueFor($config)],
default => [null, null], default => [null, null],
}; };

View file

@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Services\Automation;
/**
* Backend mirror of the Webhook node's payload-template contract: the template
* is parsed as JSON before placeholders are resolved (see RunWebhookNode), so a
* template with unquoted `{{ }}` placeholders or any malformed JSON can never
* run. An empty or literal-`null` template means "no body" and is valid.
*/
final class WebhookNodeValidator
{
/**
* First compliance issue for a webhook node's config, or null when valid.
*
* @param array<string, mixed> $config
*/
public function issueFor(array $config): ?string
{
$template = trim((string) data_get($config, 'payload_template', ''));
if ($template === '' || $template === 'null') {
return null;
}
json_decode($template);
if (json_last_error() !== JSON_ERROR_NONE) {
return __('automations.errors.webhook_invalid_payload_json');
}
return null;
}
}

View file

@ -0,0 +1,229 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\DataTransferObjects\MediaItem;
use App\Enums\Media\Type;
use App\Enums\Webhook\EventType as WebhookEvent;
use App\Jobs\DispatchWebhook;
use App\Models\Post;
use App\Models\PostPlatform;
use App\Models\SocialAccount;
use App\Models\User;
use App\Models\Webhook;
use App\Models\Workspace;
use App\Models\WorkspaceLabel;
use App\Services\Brand\SafeHttpFetcher;
use Exception;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use RuntimeException;
class WebhookService
{
public function __construct(private SafeHttpFetcher $safeHttp) {}
/**
* @param array<string, mixed> $body
*/
public function signature(array $body, string $secret): string
{
return hash_hmac('sha256', json_encode($body), $secret);
}
/**
* @throws RuntimeException
*/
public function assertEndpointAllowed(string $endpoint): void
{
try {
$this->safeHttp->guardAgainstSsrf($endpoint);
} catch (RuntimeException) {
throw new RuntimeException(__('webhooks.errors.endpoint_not_allowed'));
}
}
/**
* @throws RuntimeException
*/
public function ping(string $endpoint, string $signingSecret): void
{
$this->assertEndpointAllowed($endpoint);
$body = [
'id' => (string) Str::uuid(),
'type' => 'webhook.test',
'data' => (object) [],
'created_at' => now()->toIso8601String(),
];
try {
$response = Http::timeout(5)
->withUserAgent(config('trypost.user_agent'))
->withOptions(['allow_redirects' => false])
->asJson()
->withHeaders([
'X-Webhook-Signature' => $this->signature($body, $signingSecret),
])
->post($endpoint, $body);
} catch (Exception) {
throw new RuntimeException(__('webhooks.errors.endpoint_unreachable'));
}
if (! $response->successful()) {
throw new RuntimeException(__('webhooks.errors.endpoint_http_status', [
'status' => $response->status(),
]));
}
}
/**
* @param array<string, mixed> $payload
*/
public function dispatch(Workspace $workspace, WebhookEvent $event, array $payload): void
{
$webhooks = Webhook::query()
->where('workspace_id', $workspace->id)
->enabled()
->get();
foreach ($webhooks as $webhook) {
if (in_array($event->value, $webhook->events ?? [], true)) {
DispatchWebhook::dispatch($webhook, $event->value, $payload);
}
}
}
/**
* @return array<string, mixed>
*/
public function postPayload(Post $post): array
{
$post->load(['user', 'workspace', 'labels', 'postPlatforms.socialAccount']);
return [
'id' => $post->id,
'workspace_id' => $post->workspace_id,
'user_id' => $post->user_id,
'status' => $post->status->value,
'created_via' => $post->created_via?->value,
'content' => $post->content,
'scheduled_at' => $post->scheduled_at?->toIso8601String(),
'published_at' => $post->published_at?->toIso8601String(),
'created_at' => $post->created_at?->toIso8601String(),
'updated_at' => $post->updated_at?->toIso8601String(),
'author' => $this->authorPayload($post->user),
'workspace' => $this->workspacePayload($post),
'labels' => $post->labels
->map(fn (WorkspaceLabel $label): array => [
'id' => $label->id,
'name' => $label->name,
'color' => $label->color,
])
->values()
->all(),
'media' => $this->mediaPayload($post),
'platforms' => $post->postPlatforms
->map(fn (PostPlatform $platform): array => $this->platformPayload($platform))
->values()
->all(),
];
}
/**
* @return array{id: string, name: string}|null
*/
private function authorPayload(?User $user): ?array
{
if ($user === null) {
return null;
}
return [
'id' => $user->id,
'name' => $user->name,
];
}
/**
* @return array{id: string, name: string|null}
*/
private function workspacePayload(Post $post): array
{
return [
'id' => $post->workspace_id,
'name' => $post->workspace?->name,
];
}
/**
* @return list<array<string, mixed>>
*/
private function mediaPayload(Post $post): array
{
return collect($post->media ?? [])
->map(function (mixed $item): array {
$media = MediaItem::fromArray(is_array($item) ? $item : []);
return [
'id' => $media->id,
'path' => $media->path,
'url' => $media->url,
'type' => Type::classify($media->mime_type, $media->path)?->value,
'mime_type' => $media->mime_type,
'original_filename' => $media->original_filename,
'source' => $media->source?->value,
'source_meta' => $media->source_meta,
'meta' => $media->meta,
];
})
->values()
->all();
}
/**
* @return array<string, mixed>
*/
private function platformPayload(PostPlatform $platform): array
{
return [
'id' => $platform->id,
'social_account_id' => $platform->social_account_id,
'platform' => $platform->platform?->value,
'content_type' => $platform->content_type?->value,
'enabled' => $platform->enabled,
'status' => $platform->status?->value,
'platform_post_id' => $platform->platform_post_id,
'platform_url' => $platform->platform_url,
'published_at' => $platform->published_at?->toIso8601String(),
'error_message' => $platform->error_message,
'error_context' => $platform->error_context,
'display_name' => $platform->display_name,
'display_username' => $platform->display_username,
'display_avatar' => $platform->display_avatar,
'meta' => $platform->meta ?? [],
'social_account' => $this->socialAccountPayload($platform->socialAccount),
];
}
/**
* @return array{id: string, platform: string|null, display_name: string|null, username: string|null, is_active: bool, status: string|null}|null
*/
private function socialAccountPayload(?SocialAccount $account): ?array
{
if ($account === null) {
return null;
}
return [
'id' => $account->id,
'platform' => $account->platform?->value,
'display_name' => $account->display_name,
'username' => $account->username,
'is_active' => $account->is_active,
'status' => $account->status?->value,
];
}
}

View file

@ -270,6 +270,21 @@
'tries' => 1, 'tries' => 1,
'nice' => 0, 'nice' => 0,
], ],
'webhooks' => [
'connection' => 'redis',
'queue' => ['webhooks'],
'balance' => 'auto',
'autoScalingStrategy' => 'time',
'minProcesses' => 1,
'maxProcesses' => 2,
'timeout' => 60,
'maxTime' => 0,
'maxJobs' => 0,
'memory' => 256,
'tries' => 1,
'nice' => 0,
],
], ],
'environments' => [ 'environments' => [
@ -297,6 +312,12 @@
'balanceMaxShift' => 1, 'balanceMaxShift' => 1,
'balanceCooldown' => 3, 'balanceCooldown' => 3,
], ],
'webhooks' => [
'maxProcesses' => 3,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
], ],
'local' => [ 'local' => [
@ -315,6 +336,10 @@
'automations' => [ 'automations' => [
'maxProcesses' => 2, 'maxProcesses' => 2,
], ],
'webhooks' => [
'maxProcesses' => 1,
],
], ],
], ],
]; ];

View file

@ -146,7 +146,7 @@
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| Branded User-Agent applied to outbound HTTP from automation nodes | Branded User-Agent applied to outbound HTTP from automation nodes
| (webhook + http_request) so recipients know the request came from | (http_request) and workspace webhooks so recipients know the request came from
| TryPost.it. Self-hosters can override it. | TryPost.it. Self-hosters can override it.
| |
*/ */

View file

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Enums\Webhook\EventType;
use App\Enums\Webhook\Status;
use App\Models\Webhook;
use App\Models\Workspace;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Webhook>
*/
class WebhookFactory extends Factory
{
protected $model = Webhook::class;
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'workspace_id' => Workspace::factory(),
'endpoint' => fake()->url(),
'events' => [EventType::PostPublished->value, EventType::PostFailed->value],
'status' => Status::Enabled,
'signing_secret' => Webhook::generateSigningSecret(),
'consecutive_failures' => 0,
];
}
public function disabled(): static
{
return $this->state(fn (array $attributes): array => [
'status' => Status::Disabled,
]);
}
public function paused(): static
{
return $this->state(fn (array $attributes): array => [
'status' => Status::Paused,
'consecutive_failures' => 5,
'paused_at' => now(),
]);
}
}

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Database\Factories;
use App\Enums\Webhook\EventType;
use App\Models\Webhook;
use App\Models\WebhookLog;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<WebhookLog>
*/
class WebhookLogFactory extends Factory
{
protected $model = WebhookLog::class;
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'webhook_id' => Webhook::factory(),
'event_type' => EventType::PostPublished->value,
'payload' => [
'type' => EventType::PostPublished->value,
'data' => [],
],
'response_status' => 200,
'response_body' => 'OK',
'delivered_at' => now(),
'attempts' => 1,
];
}
public function failed(): static
{
return $this->state(fn (array $attributes): array => [
'response_status' => 500,
'response_body' => 'Internal Server Error',
'delivered_at' => null,
'failed_at' => now(),
]);
}
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('webhooks', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('workspace_id')->constrained('workspaces')->cascadeOnDelete();
$table->string('endpoint');
$table->json('events');
$table->string('status');
$table->text('signing_secret');
$table->unsignedInteger('consecutive_failures')->default(0);
$table->timestamp('paused_at')->nullable();
$table->timestamp('last_sent_at')->nullable();
$table->timestamps();
$table->index(['workspace_id', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('webhooks');
}
};

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('webhook_logs', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('webhook_id')->constrained('webhooks')->cascadeOnDelete();
$table->string('event_type');
$table->json('payload')->nullable();
$table->unsignedSmallInteger('response_status')->nullable();
$table->text('response_body')->nullable();
$table->timestamp('delivered_at')->nullable();
$table->timestamp('failed_at')->nullable();
$table->unsignedTinyInteger('attempts')->default(0);
$table->timestamps();
$table->index('webhook_id');
});
}
public function down(): void
{
Schema::dropIfExists('webhook_logs');
}
};

View file

@ -62,7 +62,6 @@
'delay' => 'تأخير', 'delay' => 'تأخير',
'condition' => 'شرط', 'condition' => 'شرط',
'publish' => 'نشر', 'publish' => 'نشر',
'webhook' => 'Webhook',
'end' => 'إنهاء', 'end' => 'إنهاء',
'fetch_rss' => 'جلب RSS', 'fetch_rss' => 'جلب RSS',
'http_request' => 'طلب HTTP', 'http_request' => 'طلب HTTP',
@ -213,7 +212,6 @@
'delay' => 'تأخير', 'delay' => 'تأخير',
'condition' => 'شرط', 'condition' => 'شرط',
'publish' => 'نشر', 'publish' => 'نشر',
'webhook' => 'Webhook',
'end' => 'إنهاء', 'end' => 'إنهاء',
'end_summary' => 'يوقف الأتمتة هنا', 'end_summary' => 'يوقف الأتمتة هنا',
'fetch_rss' => 'جلب RSS', 'fetch_rss' => 'جلب RSS',
@ -326,11 +324,6 @@
'scheduled_offset' => 'الإزاحة عن المُشغّل (بالدقائق)', 'scheduled_offset' => 'الإزاحة عن المُشغّل (بالدقائق)',
'offset_summary' => ':mode · +:offset د', 'offset_summary' => ':mode · +:offset د',
], ],
'webhook' => [
'url' => 'الرابط',
'method' => 'الطريقة',
'payload_template' => 'قالب الحمولة (JSON)',
],
'end' => [ 'end' => [
'reason' => 'السبب (اختياري)', 'reason' => 'السبب (اختياري)',
'reason_placeholder' => 'مثال: تمت تصفيته بواسطة شرط', 'reason_placeholder' => 'مثال: تمت تصفيته بواسطة شرط',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'يحتوي مخطط الأتمتة على حلقة.', 'graph_contains_cycle' => 'يحتوي مخطط الأتمتة على حلقة.',
'only_failed_can_retry' => 'يمكن إعادة محاولة عمليات التشغيل الفاشلة فقط.', 'only_failed_can_retry' => 'يمكن إعادة محاولة عمليات التشغيل الفاشلة فقط.',
'no_generated_post' => 'لم يتم العثور على منشور مُنشأ في التشغيل.', 'no_generated_post' => 'لم يتم العثور على منشور مُنشأ في التشغيل.',
'webhook_server_error' => 'خطأ في خادم Webhook.',
'webhook_request_failed' => 'تعذر إكمال طلب Webhook.',
'webhook_missing_url' => 'عقدة Webhook تفتقد إلى رابط.',
'webhook_invalid_payload_json' => 'قالب الحمولة ليس JSON صالحًا.',
'url_not_allowed' => 'رابط الطلب يشير إلى عنوان خاص أو غير قابل للوصول وتم حظره.', 'url_not_allowed' => 'رابط الطلب يشير إلى عنوان خاص أو غير قابل للوصول وتم حظره.',
'node_no_longer_exists' => 'العقدة :node_id لم تعد موجودة في الأتمتة.', 'node_no_longer_exists' => 'العقدة :node_id لم تعد موجودة في الأتمتة.',
'no_trigger_connection' => 'لا توجد عقدة متصلة بعقدة المُشغّل.', 'no_trigger_connection' => 'لا توجد عقدة متصلة بعقدة المُشغّل.',

View file

@ -16,6 +16,7 @@
'signatures' => 'التوقيعات', 'signatures' => 'التوقيعات',
'labels' => 'التسميات', 'labels' => 'التسميات',
'assets' => 'الوسائط', 'assets' => 'الوسائط',
'webhooks' => 'Webhooks',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'اللغة: :name', 'language' => 'اللغة: :name',

139
lang/ar/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'تلقَّ إشعارات فورية عند إنشاء المنشورات أو جدولتها أو إلغاء جدولتها أو نشرها أو فشلها.',
'new' => 'إنشاء ويب هوك',
'empty_title' => 'لا توجد ويب هوكس بعد',
'empty_description' => 'أنشئ ويب هوكًا لتلقي إشعارات الأحداث فور حدوثها.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'يستمع إلى',
'status' => 'الحالة',
'last_sent' => 'آخر إرسال',
],
'events_count' => '{1} حدث واحد|[2,*] :count أحداث',
'never' => 'أبدًا',
'status' => [
'enabled' => 'مفعّل',
'disabled' => 'معطّل',
'paused' => 'متوقف مؤقتًا',
],
'actions' => [
'view' => 'عرض التفاصيل',
'copy_id' => 'نسخ معرّف الويب هوك',
'delete' => 'حذف',
'edit' => 'تعديل الـ endpoint',
'enable' => 'تفعيل الـ endpoint',
'disable' => 'تعطيل الـ endpoint',
'rotate' => 'تدوير سر التوقيع',
'send_test' => 'إرسال حدث تجريبي',
'replay' => 'إعادة الإرسال',
'reveal_secret' => 'إظهار السر',
'hide_secret' => 'إخفاء السر',
'copy_secret' => 'نسخ السر',
],
'create' => [
'title' => 'إنشاء ويب هوك',
'description' => 'اضبط endpoint لتلقي إشعارات الويب هوك.',
'endpoint' => 'رابط الـ endpoint',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'الأحداث',
'events_placeholder' => 'اختر الأحداث...',
'events_selected' => '{1} حدث واحد محدد|[2,*] :count أحداث محددة',
'search_events' => 'البحث في الأحداث...',
'no_events' => 'لم يُعثر على أحداث',
'submit' => 'إنشاء ويب هوك',
'cancel' => 'إلغاء',
],
'edit' => [
'title' => 'تعديل الـ endpoint',
'description' => 'حدّث رابط الـ endpoint والأحداث المطلوب الاستماع إليها.',
'submit' => 'حفظ التغييرات',
'cancel' => 'إلغاء',
],
'delete' => [
'title' => 'حذف الويب هوك',
'description' => 'هل تريد حذف هذا الويب هوك؟ لن تتلقى بعد الآن إشعارات الأحداث على هذا الـ endpoint.',
'confirm' => 'حذف الويب هوك',
'cancel' => 'إلغاء',
],
'rotate' => [
'title' => 'تدوير سر التوقيع',
'description' => 'سيُنشأ سر توقيع جديد. يتوقف السر الحالي عن العمل فورًا. حدّث الـ endpoint لاستخدام السر الجديد.',
'submit' => 'تدوير السر',
'cancel' => 'إلغاء',
],
'show' => [
'signing_secret' => 'سر التوقيع',
'last_sent' => 'آخر إرسال :time',
'listening_for' => 'يستمع إلى',
'http_status' => 'حالة HTTP',
'status_code' => ':code - :reason',
'attempts' => 'المحاولات',
'delivered_at' => 'سُلِّم في',
'response_body' => 'نص الاستجابة',
'response' => 'الاستجابة',
'no_response_body' => 'لا يوجد نص استجابة',
'no_response' => 'لا توجد استجابة',
'payload' => 'حمولة الرسالة',
'empty_title' => 'لا توجد أحداث بعد',
'empty_description' => 'عند إنشاء المنشورات أو جدولتها أو إلغاء جدولتها أو نشرها، ستظهر أحداث الويب هوك هنا.',
],
'events' => [
'group_posts' => 'المنشورات',
'post_created' => 'تم إنشاء المنشور',
'post_scheduled' => 'تمت جدولة المنشور',
'post_unscheduled' => 'ألغيت جدولة المنشور',
'post_published' => 'تم نشر المنشور',
'post_partially_published' => 'نُشر المنشور جزئيًا',
'post_failed' => 'فشل المنشور',
'post_deleted' => 'تم حذف المنشور',
],
'http_reasons' => [
'unknown' => 'غير معروف',
'200' => 'OK',
'201' => 'تم الإنشاء',
'202' => 'مقبول',
'204' => 'لا محتوى',
'400' => 'طلب غير صالح',
'401' => 'غير مصرّح',
'403' => 'محظور',
'404' => 'غير موجود',
'408' => 'انتهت مهلة الطلب',
'422' => 'كيان غير قابل للمعالجة',
'429' => 'طلبات كثيرة جدًا',
'500' => 'خطأ داخلي في الخادم',
'502' => 'بوابة غير صالحة',
'503' => 'الخدمة غير متاحة',
'504' => 'انتهت مهلة البوابة',
],
'copied' => [
'id' => 'تم نسخ معرّف الويب هوك إلى الحافظة',
'secret' => 'تم نسخ سر التوقيع إلى الحافظة',
'response' => 'تم نسخ نص الاستجابة',
'payload' => 'تم نسخ الحمولة',
],
'errors' => [
'endpoint_not_allowed' => 'هذا الـ endpoint غير مسموح به.',
'endpoint_unreachable' => 'الـ endpoint غير قابل للوصول.',
'endpoint_http_status' => 'أعاد الـ endpoint رمز HTTP :status.',
],
'flash' => [
'created' => 'تم إنشاء الويب هوك.',
'updated' => 'تم تحديث الويب هوك.',
'deleted' => 'تم حذف الويب هوك.',
'secret_rotated' => 'تم تدوير سر التوقيع.',
'replayed' => 'أُعيد إرسال حدث الويب هوك.',
'tested' => 'أُرسل الحدث التجريبي.',
],
'mail' => [
'paused_subject' => 'تم إيقاف الويب هوك مؤقتًا: :endpoint',
'paused_title' => 'أُوقف الويب هوك مؤقتًا بعد إخفاقات متكررة',
'paused_preview' => 'أوقفنا ويب هوكًا مؤقتًا بعد 5 إخفاقات تسليم متتالية.',
'paused_body' => 'أوقفنا الويب هوك على :endpoint مؤقتًا بعد 5 إخفاقات تسليم متتالية. راجع الـ endpoint وأعد تفعيله من صفحة تفاصيل الويب هوك.',
'paused_cta' => 'عرض الويب هوك',
],
];

View file

@ -62,7 +62,6 @@
'delay' => 'Verzögerung', 'delay' => 'Verzögerung',
'condition' => 'Bedingung', 'condition' => 'Bedingung',
'publish' => 'Veröffentlichen', 'publish' => 'Veröffentlichen',
'webhook' => 'Webhook',
'end' => 'Ende', 'end' => 'Ende',
'fetch_rss' => 'RSS abrufen', 'fetch_rss' => 'RSS abrufen',
'http_request' => 'HTTP-Anfrage', 'http_request' => 'HTTP-Anfrage',
@ -213,7 +212,6 @@
'delay' => 'Verzögerung', 'delay' => 'Verzögerung',
'condition' => 'Bedingung', 'condition' => 'Bedingung',
'publish' => 'Veröffentlichen', 'publish' => 'Veröffentlichen',
'webhook' => 'Webhook',
'end' => 'Ende', 'end' => 'Ende',
'end_summary' => 'Stoppt die Automatisierung hier', 'end_summary' => 'Stoppt die Automatisierung hier',
'fetch_rss' => 'RSS abrufen', 'fetch_rss' => 'RSS abrufen',
@ -326,11 +324,6 @@
'scheduled_offset' => 'Versatz zum Trigger (Minuten)', 'scheduled_offset' => 'Versatz zum Trigger (Minuten)',
'offset_summary' => ':mode · +:offset Min.', 'offset_summary' => ':mode · +:offset Min.',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'Methode',
'payload_template' => 'Payload-Vorlage (JSON)',
],
'end' => [ 'end' => [
'reason' => 'Grund (optional)', 'reason' => 'Grund (optional)',
'reason_placeholder' => 'z. B. Durch Bedingung herausgefiltert', 'reason_placeholder' => 'z. B. Durch Bedingung herausgefiltert',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'Der Automatisierungsgraph enthält einen Zyklus.', 'graph_contains_cycle' => 'Der Automatisierungsgraph enthält einen Zyklus.',
'only_failed_can_retry' => 'Nur fehlgeschlagene Ausführungen können wiederholt werden.', 'only_failed_can_retry' => 'Nur fehlgeschlagene Ausführungen können wiederholt werden.',
'no_generated_post' => 'Bei der Ausführung wurde kein generierter Beitrag gefunden.', 'no_generated_post' => 'Bei der Ausführung wurde kein generierter Beitrag gefunden.',
'webhook_server_error' => 'Webhook-Serverfehler.',
'webhook_request_failed' => 'Die Webhook-Anfrage konnte nicht abgeschlossen werden.',
'webhook_missing_url' => 'Dem Webhook-Node fehlt eine URL.',
'webhook_invalid_payload_json' => 'Die Payload-Vorlage ist kein gültiges JSON.',
'url_not_allowed' => 'Die Anfrage-URL verweist auf eine private oder nicht erreichbare Adresse und wurde blockiert.', 'url_not_allowed' => 'Die Anfrage-URL verweist auf eine private oder nicht erreichbare Adresse und wurde blockiert.',
'node_no_longer_exists' => 'Node :node_id existiert in der Automatisierung nicht mehr.', 'node_no_longer_exists' => 'Node :node_id existiert in der Automatisierung nicht mehr.',
'no_trigger_connection' => 'Kein Node mit dem Trigger-Node verbunden.', 'no_trigger_connection' => 'Kein Node mit dem Trigger-Node verbunden.',

View file

@ -16,6 +16,7 @@
'signatures' => 'Signaturen', 'signatures' => 'Signaturen',
'labels' => 'Labels', 'labels' => 'Labels',
'assets' => 'Assets', 'assets' => 'Assets',
'webhooks' => 'Webhooks',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'Sprache: :name', 'language' => 'Sprache: :name',

139
lang/de/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'Erhalte Echtzeit-Benachrichtigungen, wenn Beiträge erstellt, geplant, deren Planung aufgehoben, veröffentlicht werden oder fehlschlagen.',
'new' => 'Webhook erstellen',
'empty_title' => 'Noch keine Webhooks',
'empty_description' => 'Erstelle einen Webhook, um Ereignisbenachrichtigungen in Echtzeit zu empfangen.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'Hört auf',
'status' => 'Status',
'last_sent' => 'Zuletzt gesendet',
],
'events_count' => '{1} :count Ereignis|[2,*] :count Ereignisse',
'never' => 'Nie',
'status' => [
'enabled' => 'Aktiv',
'disabled' => 'Deaktiviert',
'paused' => 'Pausiert',
],
'actions' => [
'view' => 'Details anzeigen',
'copy_id' => 'Webhook-ID kopieren',
'delete' => 'Löschen',
'edit' => 'Endpoint bearbeiten',
'enable' => 'Endpoint aktivieren',
'disable' => 'Endpoint deaktivieren',
'rotate' => 'Signatur-Secret rotieren',
'send_test' => 'Test-Ereignis senden',
'replay' => 'Erneut senden',
'reveal_secret' => 'Secret anzeigen',
'hide_secret' => 'Secret ausblenden',
'copy_secret' => 'Secret kopieren',
],
'create' => [
'title' => 'Webhook erstellen',
'description' => 'Konfiguriere einen Endpoint, um Webhook-Benachrichtigungen zu empfangen.',
'endpoint' => 'Endpoint-URL',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'Ereignisse',
'events_placeholder' => 'Ereignisse auswählen...',
'events_selected' => '{1} :count Ereignis ausgewählt|[2,*] :count Ereignisse ausgewählt',
'search_events' => 'Ereignisse suchen...',
'no_events' => 'Keine Ereignisse gefunden',
'submit' => 'Webhook erstellen',
'cancel' => 'Abbrechen',
],
'edit' => [
'title' => 'Endpoint bearbeiten',
'description' => 'Aktualisiere die Endpoint-URL und die Ereignisse, auf die gehört wird.',
'submit' => 'Änderungen speichern',
'cancel' => 'Abbrechen',
],
'delete' => [
'title' => 'Webhook löschen',
'description' => 'Möchtest du diesen Webhook wirklich löschen? Du erhältst an diesem Endpoint keine Ereignisbenachrichtigungen mehr.',
'confirm' => 'Webhook löschen',
'cancel' => 'Abbrechen',
],
'rotate' => [
'title' => 'Signatur-Secret rotieren',
'description' => 'Dadurch wird ein neues Signatur-Secret erzeugt. Das aktuelle Secret funktioniert sofort nicht mehr. Aktualisiere deinen Endpoint auf das neue Secret.',
'submit' => 'Secret rotieren',
'cancel' => 'Abbrechen',
],
'show' => [
'signing_secret' => 'Signatur-Secret',
'last_sent' => 'Zuletzt gesendet :time',
'listening_for' => 'Hört auf',
'http_status' => 'HTTP-Status',
'status_code' => ':code - :reason',
'attempts' => 'Versuche',
'delivered_at' => 'Zugestellt um',
'response_body' => 'Antworttext',
'response' => 'Antwort',
'no_response_body' => 'Kein Antworttext',
'no_response' => 'Keine Antwort',
'payload' => 'Nachrichten-Payload',
'empty_title' => 'Noch keine Webhook-Ereignisse',
'empty_description' => 'Sobald Beiträge erstellt, geplant, deren Planung aufgehoben oder veröffentlicht werden, siehst du die Webhook-Ereignisse hier.',
],
'events' => [
'group_posts' => 'Beiträge',
'post_created' => 'Beitrag erstellt',
'post_scheduled' => 'Beitrag geplant',
'post_unscheduled' => 'Planung aufgehoben',
'post_published' => 'Beitrag veröffentlicht',
'post_partially_published' => 'Beitrag teilweise veröffentlicht',
'post_failed' => 'Beitrag fehlgeschlagen',
'post_deleted' => 'Beitrag gelöscht',
],
'http_reasons' => [
'unknown' => 'Unbekannt',
'200' => 'OK',
'201' => 'Erstellt',
'202' => 'Akzeptiert',
'204' => 'Kein Inhalt',
'400' => 'Ungültige Anfrage',
'401' => 'Nicht autorisiert',
'403' => 'Verboten',
'404' => 'Nicht gefunden',
'408' => 'Zeitüberschreitung',
'422' => 'Nicht verarbeitbar',
'429' => 'Zu viele Anfragen',
'500' => 'Interner Serverfehler',
'502' => 'Ungültiges Gateway',
'503' => 'Dienst nicht verfügbar',
'504' => 'Gateway-Zeitüberschreitung',
],
'copied' => [
'id' => 'Webhook-ID in die Zwischenablage kopiert',
'secret' => 'Signatur-Secret in die Zwischenablage kopiert',
'response' => 'Antworttext kopiert',
'payload' => 'Payload kopiert',
],
'errors' => [
'endpoint_not_allowed' => 'Dieser Endpoint ist nicht erlaubt.',
'endpoint_unreachable' => 'Der Endpoint ist nicht erreichbar.',
'endpoint_http_status' => 'Der Endpoint hat HTTP :status zurückgegeben.',
],
'flash' => [
'created' => 'Webhook erstellt.',
'updated' => 'Webhook aktualisiert.',
'deleted' => 'Webhook gelöscht.',
'secret_rotated' => 'Signatur-Secret rotiert.',
'replayed' => 'Webhook-Ereignis erneut gesendet.',
'tested' => 'Test-Ereignis gesendet.',
],
'mail' => [
'paused_subject' => 'Webhook pausiert: :endpoint',
'paused_title' => 'Webhook nach wiederholten Fehlern pausiert',
'paused_preview' => 'Wir haben einen Webhook nach 5 aufeinanderfolgenden Zustellfehlern pausiert.',
'paused_body' => 'Wir haben den Webhook unter :endpoint nach 5 aufeinanderfolgenden Zustellfehlern pausiert. Prüfe den Endpoint und aktiviere ihn wieder auf der Webhook-Detailseite.',
'paused_cta' => 'Webhook anzeigen',
],
];

View file

@ -62,7 +62,6 @@
'delay' => 'Καθυστέρηση', 'delay' => 'Καθυστέρηση',
'condition' => 'Συνθήκη', 'condition' => 'Συνθήκη',
'publish' => 'Δημοσίευση', 'publish' => 'Δημοσίευση',
'webhook' => 'Webhook',
'end' => 'Τέλος', 'end' => 'Τέλος',
'fetch_rss' => 'Ανάκτηση RSS', 'fetch_rss' => 'Ανάκτηση RSS',
'http_request' => 'Αίτημα HTTP', 'http_request' => 'Αίτημα HTTP',
@ -213,7 +212,6 @@
'delay' => 'Καθυστέρηση', 'delay' => 'Καθυστέρηση',
'condition' => 'Συνθήκη', 'condition' => 'Συνθήκη',
'publish' => 'Δημοσίευση', 'publish' => 'Δημοσίευση',
'webhook' => 'Webhook',
'end' => 'Τέλος', 'end' => 'Τέλος',
'end_summary' => 'Σταματά τον αυτοματισμό εδώ', 'end_summary' => 'Σταματά τον αυτοματισμό εδώ',
'fetch_rss' => 'Ανάκτηση RSS', 'fetch_rss' => 'Ανάκτηση RSS',
@ -326,11 +324,6 @@
'scheduled_offset' => 'Μετατόπιση από το έναυσμα (λεπτά)', 'scheduled_offset' => 'Μετατόπιση από το έναυσμα (λεπτά)',
'offset_summary' => ':mode · +:offset λεπτά', 'offset_summary' => ':mode · +:offset λεπτά',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'Μέθοδος',
'payload_template' => 'Πρότυπο payload (JSON)',
],
'end' => [ 'end' => [
'reason' => 'Αιτία (προαιρετικό)', 'reason' => 'Αιτία (προαιρετικό)',
'reason_placeholder' => 'π.χ. Φιλτραρίστηκε από τη συνθήκη', 'reason_placeholder' => 'π.χ. Φιλτραρίστηκε από τη συνθήκη',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'Το γράφημα του αυτοματισμού περιέχει κύκλο.', 'graph_contains_cycle' => 'Το γράφημα του αυτοματισμού περιέχει κύκλο.',
'only_failed_can_retry' => 'Μόνο οι αποτυχημένες εκτελέσεις μπορούν να επαναληφθούν.', 'only_failed_can_retry' => 'Μόνο οι αποτυχημένες εκτελέσεις μπορούν να επαναληφθούν.',
'no_generated_post' => 'Δεν βρέθηκε δημιουργημένη δημοσίευση στην εκτέλεση.', 'no_generated_post' => 'Δεν βρέθηκε δημιουργημένη δημοσίευση στην εκτέλεση.',
'webhook_server_error' => 'Σφάλμα διακομιστή webhook.',
'webhook_request_failed' => 'Το αίτημα webhook δεν ήταν δυνατό να ολοκληρωθεί.',
'webhook_missing_url' => 'Από τον κόμβο webhook λείπει μια διεύθυνση URL.',
'webhook_invalid_payload_json' => 'Το πρότυπο payload δεν είναι έγκυρο JSON.',
'url_not_allowed' => 'Η διεύθυνση URL του αιτήματος δείχνει σε ιδιωτική ή μη προσβάσιμη διεύθυνση και αποκλείστηκε.', 'url_not_allowed' => 'Η διεύθυνση URL του αιτήματος δείχνει σε ιδιωτική ή μη προσβάσιμη διεύθυνση και αποκλείστηκε.',
'node_no_longer_exists' => 'Ο κόμβος :node_id δεν υπάρχει πλέον στον αυτοματισμό.', 'node_no_longer_exists' => 'Ο κόμβος :node_id δεν υπάρχει πλέον στον αυτοματισμό.',
'no_trigger_connection' => 'Κανένας κόμβος δεν είναι συνδεδεμένος με τον κόμβο εναύσματος.', 'no_trigger_connection' => 'Κανένας κόμβος δεν είναι συνδεδεμένος με τον κόμβο εναύσματος.',

View file

@ -16,6 +16,7 @@
'signatures' => 'Υπογραφές', 'signatures' => 'Υπογραφές',
'labels' => 'Ετικέτες', 'labels' => 'Ετικέτες',
'assets' => 'Στοιχεία', 'assets' => 'Στοιχεία',
'webhooks' => 'Webhooks',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'Γλώσσα: :name', 'language' => 'Γλώσσα: :name',

139
lang/el/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'Λάβετε ειδοποιήσεις σε πραγματικό χρόνο όταν δημιουργούνται, προγραμματίζονται, αποπρογραμματίζονται, δημοσιεύονται ή αποτυγχάνουν αναρτήσεις.',
'new' => 'Δημιουργία webhook',
'empty_title' => 'Δεν υπάρχουν ακόμα webhooks',
'empty_description' => 'Δημιουργήστε ένα webhook για να λαμβάνετε ειδοποιήσεις συμβάντων σε πραγματικό χρόνο.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'Ακούει',
'status' => 'Κατάσταση',
'last_sent' => 'Τελευταία αποστολή',
],
'events_count' => '{1} :count συμβάν|[2,*] :count συμβάντα',
'never' => 'Ποτέ',
'status' => [
'enabled' => 'Ενεργό',
'disabled' => 'Ανενεργό',
'paused' => 'Σε παύση',
],
'actions' => [
'view' => 'Προβολή λεπτομερειών',
'copy_id' => 'Αντιγραφή ID webhook',
'delete' => 'Διαγραφή',
'edit' => 'Επεξεργασία endpoint',
'enable' => 'Ενεργοποίηση endpoint',
'disable' => 'Απενεργοποίηση endpoint',
'rotate' => 'Αλλαγή μυστικού υπογραφής',
'send_test' => 'Αποστολή δοκιμαστικού συμβάντος',
'replay' => 'Επανάληψη αποστολής',
'reveal_secret' => 'Εμφάνιση μυστικού',
'hide_secret' => 'Απόκρυψη μυστικού',
'copy_secret' => 'Αντιγραφή μυστικού',
],
'create' => [
'title' => 'Δημιουργία webhook',
'description' => 'Ρυθμίστε ένα endpoint για να λαμβάνετε ειδοποιήσεις webhook.',
'endpoint' => 'URL του endpoint',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'Συμβάντα',
'events_placeholder' => 'Επιλογή συμβάντων...',
'events_selected' => '{1} :count συμβάν επιλεγμένο|[2,*] :count συμβάντα επιλεγμένα',
'search_events' => 'Αναζήτηση συμβάντων...',
'no_events' => 'Δεν βρέθηκαν συμβάντα',
'submit' => 'Δημιουργία webhook',
'cancel' => 'Ακύρωση',
],
'edit' => [
'title' => 'Επεξεργασία endpoint',
'description' => 'Ενημερώστε το URL του endpoint και τα συμβάντα που θα ακούει.',
'submit' => 'Αποθήκευση αλλαγών',
'cancel' => 'Ακύρωση',
],
'delete' => [
'title' => 'Διαγραφή webhook',
'description' => 'Θέλετε σίγουρα να διαγράψετε αυτό το webhook; Δεν θα λαμβάνετε πλέον ειδοποιήσεις συμβάντων σε αυτό το endpoint.',
'confirm' => 'Διαγραφή webhook',
'cancel' => 'Ακύρωση',
],
'rotate' => [
'title' => 'Αλλαγή μυστικού υπογραφής',
'description' => 'Θα δημιουργηθεί ένα νέο μυστικό υπογραφής. Το τρέχον μυστικό σταματά να λειτουργεί αμέσως. Ενημερώστε το endpoint σας ώστε να χρησιμοποιεί το νέο μυστικό.',
'submit' => 'Αλλαγή μυστικού',
'cancel' => 'Ακύρωση',
],
'show' => [
'signing_secret' => 'Μυστικό υπογραφής',
'last_sent' => 'Τελευταία αποστολή :time',
'listening_for' => 'Ακούει',
'http_status' => 'Κατάσταση HTTP',
'status_code' => ':code - :reason',
'attempts' => 'Προσπάθειες',
'delivered_at' => 'Παραδόθηκε στις',
'response_body' => 'Σώμα απάντησης',
'response' => 'Απάντηση',
'no_response_body' => 'Χωρίς σώμα απάντησης',
'no_response' => 'Χωρίς απάντηση',
'payload' => 'Payload μηνύματος',
'empty_title' => 'Δεν υπάρχουν ακόμα συμβάντα',
'empty_description' => 'Όταν δημιουργούνται, προγραμματίζονται, αποπρογραμματίζονται ή δημοσιεύονται αναρτήσεις, τα συμβάντα του webhook εμφανίζονται εδώ.',
],
'events' => [
'group_posts' => 'Αναρτήσεις',
'post_created' => 'Η ανάρτηση δημιουργήθηκε',
'post_scheduled' => 'Η ανάρτηση προγραμματίστηκε',
'post_unscheduled' => 'Ακυρώθηκε ο προγραμματισμός',
'post_published' => 'Η ανάρτηση δημοσιεύτηκε',
'post_partially_published' => 'Μερική δημοσίευση',
'post_failed' => 'Η ανάρτηση απέτυχε',
'post_deleted' => 'Η ανάρτηση διαγράφηκε',
],
'http_reasons' => [
'unknown' => 'Άγνωστο',
'200' => 'OK',
'201' => 'Δημιουργήθηκε',
'202' => 'Αποδεκτό',
'204' => 'Χωρίς περιεχόμενο',
'400' => 'Εσφαλμένο αίτημα',
'401' => 'Μη εξουσιοδοτημένο',
'403' => 'Απαγορευμένο',
'404' => 'Δεν βρέθηκε',
'408' => 'Λήξη χρόνου αιτήματος',
'422' => 'Οντότητα που δεν μπορεί να επεξεργαστεί',
'429' => 'Πάρα πολλά αιτήματα',
'500' => 'Εσωτερικό σφάλμα διακομιστή',
'502' => 'Μη έγκυρη πύλη',
'503' => 'Η υπηρεσία δεν είναι διαθέσιμη',
'504' => 'Λήξη χρόνου πύλης',
],
'copied' => [
'id' => 'Το ID του webhook αντιγράφηκε στο πρόχειρο',
'secret' => 'Το μυστικό υπογραφής αντιγράφηκε στο πρόχειρο',
'response' => 'Το σώμα απάντησης αντιγράφηκε',
'payload' => 'Το payload αντιγράφηκε',
],
'errors' => [
'endpoint_not_allowed' => 'Αυτό το endpoint δεν επιτρέπεται.',
'endpoint_unreachable' => 'Το endpoint δεν είναι προσβάσιμο.',
'endpoint_http_status' => 'Το endpoint επέστρεψε HTTP :status.',
],
'flash' => [
'created' => 'Το webhook δημιουργήθηκε.',
'updated' => 'Το webhook ενημερώθηκε.',
'deleted' => 'Το webhook διαγράφηκε.',
'secret_rotated' => 'Το μυστικό υπογραφής άλλαξε.',
'replayed' => 'Το συμβάν του webhook στάλθηκε ξανά.',
'tested' => 'Το δοκιμαστικό συμβάν στάλθηκε.',
],
'mail' => [
'paused_subject' => 'Το webhook τέθηκε σε παύση: :endpoint',
'paused_title' => 'Το webhook τέθηκε σε παύση μετά από επαναλαμβανόμενες αποτυχίες',
'paused_preview' => 'Θέσαμε ένα webhook σε παύση μετά από 5 διαδοχικές αποτυχίες παράδοσης.',
'paused_body' => 'Θέσαμε το webhook στο :endpoint σε παύση μετά από 5 διαδοχικές αποτυχίες παράδοσης. Ελέγξτε το endpoint και ενεργοποιήστε το ξανά από τη σελίδα λεπτομερειών του webhook.',
'paused_cta' => 'Προβολή webhook',
],
];

View file

@ -62,7 +62,6 @@
'delay' => 'Delay', 'delay' => 'Delay',
'condition' => 'Condition', 'condition' => 'Condition',
'publish' => 'Publish', 'publish' => 'Publish',
'webhook' => 'Webhook',
'end' => 'End', 'end' => 'End',
'fetch_rss' => 'Fetch RSS', 'fetch_rss' => 'Fetch RSS',
'http_request' => 'HTTP request', 'http_request' => 'HTTP request',
@ -213,7 +212,6 @@
'delay' => 'Delay', 'delay' => 'Delay',
'condition' => 'Condition', 'condition' => 'Condition',
'publish' => 'Publish', 'publish' => 'Publish',
'webhook' => 'Webhook',
'end' => 'End', 'end' => 'End',
'end_summary' => 'Stops the automation here', 'end_summary' => 'Stops the automation here',
'fetch_rss' => 'Fetch RSS', 'fetch_rss' => 'Fetch RSS',
@ -326,11 +324,6 @@
'scheduled_offset' => 'Offset from trigger (minutes)', 'scheduled_offset' => 'Offset from trigger (minutes)',
'offset_summary' => ':mode · +:offset min', 'offset_summary' => ':mode · +:offset min',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'Method',
'payload_template' => 'Payload template (JSON)',
],
'end' => [ 'end' => [
'reason' => 'Reason (optional)', 'reason' => 'Reason (optional)',
'reason_placeholder' => 'e.g. Filtered out by condition', 'reason_placeholder' => 'e.g. Filtered out by condition',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'Automation graph contains a cycle.', 'graph_contains_cycle' => 'Automation graph contains a cycle.',
'only_failed_can_retry' => 'Only failed runs can be retried.', 'only_failed_can_retry' => 'Only failed runs can be retried.',
'no_generated_post' => 'No generated post found on run.', 'no_generated_post' => 'No generated post found on run.',
'webhook_server_error' => 'Webhook server error.',
'webhook_request_failed' => 'The webhook request could not be completed.',
'webhook_missing_url' => 'The webhook node is missing a URL.',
'webhook_invalid_payload_json' => 'The payload template is not valid JSON.',
'url_not_allowed' => 'The request URL points to a private or unreachable address and was blocked.', 'url_not_allowed' => 'The request URL points to a private or unreachable address and was blocked.',
'node_no_longer_exists' => 'Node :node_id no longer exists in the automation.', 'node_no_longer_exists' => 'Node :node_id no longer exists in the automation.',
'no_trigger_connection' => 'No node connected to the Trigger node.', 'no_trigger_connection' => 'No node connected to the Trigger node.',

View file

@ -16,6 +16,7 @@
'signatures' => 'Signatures', 'signatures' => 'Signatures',
'labels' => 'Labels', 'labels' => 'Labels',
'assets' => 'Assets', 'assets' => 'Assets',
'webhooks' => 'Webhooks',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'Language: :name', 'language' => 'Language: :name',

139
lang/en/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'Receive real-time notifications when posts are created, scheduled, unscheduled, published, or fail.',
'new' => 'Create webhook',
'empty_title' => 'No webhooks yet',
'empty_description' => 'Create a webhook to receive real-time event notifications.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'Listening for',
'status' => 'Status',
'last_sent' => 'Last sent',
],
'events_count' => '{1} :count event|[2,*] :count events',
'never' => 'Never',
'status' => [
'enabled' => 'Enabled',
'disabled' => 'Disabled',
'paused' => 'Paused',
],
'actions' => [
'view' => 'View details',
'copy_id' => 'Copy webhook ID',
'delete' => 'Delete',
'edit' => 'Edit endpoint',
'enable' => 'Enable endpoint',
'disable' => 'Disable endpoint',
'rotate' => 'Rotate signing secret',
'send_test' => 'Send test event',
'replay' => 'Replay',
'reveal_secret' => 'Reveal secret',
'hide_secret' => 'Hide secret',
'copy_secret' => 'Copy secret',
],
'create' => [
'title' => 'Create webhook',
'description' => 'Configure an endpoint to receive webhook notifications.',
'endpoint' => 'Endpoint URL',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'Events',
'events_placeholder' => 'Select events...',
'events_selected' => '{1} :count event selected|[2,*] :count events selected',
'search_events' => 'Search events...',
'no_events' => 'No events found',
'submit' => 'Create webhook',
'cancel' => 'Cancel',
],
'edit' => [
'title' => 'Edit endpoint',
'description' => 'Update the webhook endpoint URL and events to listen for.',
'submit' => 'Save changes',
'cancel' => 'Cancel',
],
'delete' => [
'title' => 'Delete webhook',
'description' => 'Are you sure you want to delete this webhook? You will no longer receive event notifications at this endpoint.',
'confirm' => 'Delete webhook',
'cancel' => 'Cancel',
],
'rotate' => [
'title' => 'Rotate signing secret',
'description' => 'This will generate a new signing secret. The current secret will stop working immediately. Make sure to update your endpoint to use the new secret.',
'submit' => 'Rotate secret',
'cancel' => 'Cancel',
],
'show' => [
'signing_secret' => 'Signing secret',
'last_sent' => 'Last sent :time',
'listening_for' => 'Listening for',
'http_status' => 'HTTP status',
'status_code' => ':code - :reason',
'attempts' => 'Attempts',
'delivered_at' => 'Delivered at',
'response_body' => 'Response body',
'response' => 'Response',
'no_response_body' => 'No response body',
'no_response' => 'No response',
'payload' => 'Message payload',
'empty_title' => 'No webhook events yet',
'empty_description' => 'Once posts are created, scheduled, unscheduled, or published, you will see the webhook events here.',
],
'events' => [
'group_posts' => 'Posts',
'post_created' => 'Post created',
'post_scheduled' => 'Post scheduled',
'post_unscheduled' => 'Post unscheduled',
'post_published' => 'Post published',
'post_partially_published' => 'Post partially published',
'post_failed' => 'Post failed',
'post_deleted' => 'Post deleted',
],
'http_reasons' => [
'unknown' => 'Unknown',
'200' => 'OK',
'201' => 'Created',
'202' => 'Accepted',
'204' => 'No Content',
'400' => 'Bad Request',
'401' => 'Unauthorized',
'403' => 'Forbidden',
'404' => 'Not Found',
'408' => 'Request Timeout',
'422' => 'Unprocessable Entity',
'429' => 'Too Many Requests',
'500' => 'Internal Server Error',
'502' => 'Bad Gateway',
'503' => 'Service Unavailable',
'504' => 'Gateway Timeout',
],
'copied' => [
'id' => 'Webhook ID copied to clipboard',
'secret' => 'Signing secret copied to clipboard',
'response' => 'Response body copied',
'payload' => 'Payload copied',
],
'errors' => [
'endpoint_not_allowed' => 'This endpoint is not allowed.',
'endpoint_unreachable' => 'The endpoint is not reachable.',
'endpoint_http_status' => 'The endpoint returned HTTP :status.',
],
'flash' => [
'created' => 'Webhook created.',
'updated' => 'Webhook updated.',
'deleted' => 'Webhook deleted.',
'secret_rotated' => 'Signing secret rotated.',
'replayed' => 'Webhook event replayed.',
'tested' => 'Test event sent.',
],
'mail' => [
'paused_subject' => 'Webhook paused: :endpoint',
'paused_title' => 'Webhook paused after repeated failures',
'paused_preview' => 'We paused a webhook after 5 consecutive delivery failures.',
'paused_body' => 'We paused the webhook at :endpoint after 5 consecutive delivery failures. Review the endpoint and re-enable it from the webhook details page.',
'paused_cta' => 'View webhook',
],
];

View file

@ -62,7 +62,6 @@
'delay' => 'Espera', 'delay' => 'Espera',
'condition' => 'Condición', 'condition' => 'Condición',
'publish' => 'Publicar', 'publish' => 'Publicar',
'webhook' => 'Webhook',
'end' => 'Fin', 'end' => 'Fin',
'fetch_rss' => 'Obtener RSS', 'fetch_rss' => 'Obtener RSS',
'http_request' => 'Petición HTTP', 'http_request' => 'Petición HTTP',
@ -213,7 +212,6 @@
'delay' => 'Retraso', 'delay' => 'Retraso',
'condition' => 'Condición', 'condition' => 'Condición',
'publish' => 'Publicar', 'publish' => 'Publicar',
'webhook' => 'Webhook',
'end' => 'Terminar', 'end' => 'Terminar',
'end_summary' => 'Termina la automatización aquí', 'end_summary' => 'Termina la automatización aquí',
'fetch_rss' => 'Obtener RSS', 'fetch_rss' => 'Obtener RSS',
@ -326,11 +324,6 @@
'scheduled_offset' => 'Diferencia desde el disparador (minutos)', 'scheduled_offset' => 'Diferencia desde el disparador (minutos)',
'offset_summary' => ':mode · +:offset min', 'offset_summary' => ':mode · +:offset min',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'Método',
'payload_template' => 'Plantilla de payload (JSON)',
],
'end' => [ 'end' => [
'reason' => 'Razón (opcional)', 'reason' => 'Razón (opcional)',
'reason_placeholder' => 'p.ej. Filtrado por la condición', 'reason_placeholder' => 'p.ej. Filtrado por la condición',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'El grafo de la automatización contiene un ciclo.', 'graph_contains_cycle' => 'El grafo de la automatización contiene un ciclo.',
'only_failed_can_retry' => 'Solo se pueden reintentar ejecuciones fallidas.', 'only_failed_can_retry' => 'Solo se pueden reintentar ejecuciones fallidas.',
'no_generated_post' => 'No se encontró un post generado en la ejecución.', 'no_generated_post' => 'No se encontró un post generado en la ejecución.',
'webhook_server_error' => 'Error del servidor del webhook.',
'webhook_request_failed' => 'No se pudo completar la solicitud del webhook.',
'webhook_missing_url' => 'Al nodo de webhook le falta la URL.',
'webhook_invalid_payload_json' => 'La plantilla de payload no es un JSON válido.',
'url_not_allowed' => 'La URL de la petición apunta a una dirección privada o inaccesible y fue bloqueada.', 'url_not_allowed' => 'La URL de la petición apunta a una dirección privada o inaccesible y fue bloqueada.',
'node_no_longer_exists' => 'El nodo :node_id ya no existe en la automatización.', 'node_no_longer_exists' => 'El nodo :node_id ya no existe en la automatización.',
'no_trigger_connection' => 'Ningún nodo está conectado al nodo disparador.', 'no_trigger_connection' => 'Ningún nodo está conectado al nodo disparador.',

View file

@ -16,6 +16,7 @@
'signatures' => 'Firmas', 'signatures' => 'Firmas',
'labels' => 'Etiquetas', 'labels' => 'Etiquetas',
'assets' => 'Medios', 'assets' => 'Medios',
'webhooks' => 'Webhooks',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'Idioma: :name', 'language' => 'Idioma: :name',

139
lang/es/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'Recibe notificaciones en tiempo real cuando se crean, programan, desprograman, publican o fallan los posts.',
'new' => 'Crear webhook',
'empty_title' => 'Aún no hay webhooks',
'empty_description' => 'Crea un webhook para recibir notificaciones de eventos en tiempo real.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'Escuchando',
'status' => 'Estado',
'last_sent' => 'Último envío',
],
'events_count' => '{1} :count evento|[2,*] :count eventos',
'never' => 'Nunca',
'status' => [
'enabled' => 'Activo',
'disabled' => 'Desactivado',
'paused' => 'Pausado',
],
'actions' => [
'view' => 'Ver detalles',
'copy_id' => 'Copiar ID del webhook',
'delete' => 'Eliminar',
'edit' => 'Editar endpoint',
'enable' => 'Activar endpoint',
'disable' => 'Desactivar endpoint',
'rotate' => 'Rotar secret de firma',
'send_test' => 'Enviar evento de prueba',
'replay' => 'Reenviar',
'reveal_secret' => 'Mostrar secret',
'hide_secret' => 'Ocultar secret',
'copy_secret' => 'Copiar secret',
],
'create' => [
'title' => 'Crear webhook',
'description' => 'Configura un endpoint para recibir notificaciones de webhook.',
'endpoint' => 'URL del endpoint',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'Eventos',
'events_placeholder' => 'Seleccionar eventos...',
'events_selected' => '{1} :count evento seleccionado|[2,*] :count eventos seleccionados',
'search_events' => 'Buscar eventos...',
'no_events' => 'No se encontraron eventos',
'submit' => 'Crear webhook',
'cancel' => 'Cancelar',
],
'edit' => [
'title' => 'Editar endpoint',
'description' => 'Actualiza la URL del endpoint y los eventos a escuchar.',
'submit' => 'Guardar cambios',
'cancel' => 'Cancelar',
],
'delete' => [
'title' => 'Eliminar webhook',
'description' => '¿Seguro que quieres eliminar este webhook? Ya no recibirás notificaciones de eventos en este endpoint.',
'confirm' => 'Eliminar webhook',
'cancel' => 'Cancelar',
],
'rotate' => [
'title' => 'Rotar secret de firma',
'description' => 'Esto genera un nuevo secret de firma. El secret actual deja de funcionar de inmediato. Actualiza tu endpoint para usar el nuevo secret.',
'submit' => 'Rotar secret',
'cancel' => 'Cancelar',
],
'show' => [
'signing_secret' => 'Secret de firma',
'last_sent' => 'Último envío :time',
'listening_for' => 'Escuchando',
'http_status' => 'Estado HTTP',
'status_code' => ':code - :reason',
'attempts' => 'Intentos',
'delivered_at' => 'Entregado el',
'response_body' => 'Cuerpo de la respuesta',
'response' => 'Respuesta',
'no_response_body' => 'Sin cuerpo de respuesta',
'no_response' => 'Sin respuesta',
'payload' => 'Payload del mensaje',
'empty_title' => 'Aún no hay eventos',
'empty_description' => 'Cuando se creen, programen, desprogramen o publiquen posts, verás aquí los eventos del webhook.',
],
'events' => [
'group_posts' => 'Posts',
'post_created' => 'Post creado',
'post_scheduled' => 'Post programado',
'post_unscheduled' => 'Post desprogramado',
'post_published' => 'Post publicado',
'post_partially_published' => 'Post parcialmente publicado',
'post_failed' => 'Post fallido',
'post_deleted' => 'Post eliminado',
],
'http_reasons' => [
'unknown' => 'Desconocido',
'200' => 'OK',
'201' => 'Creado',
'202' => 'Aceptado',
'204' => 'Sin contenido',
'400' => 'Solicitud incorrecta',
'401' => 'No autorizado',
'403' => 'Prohibido',
'404' => 'No encontrado',
'408' => 'Tiempo de espera agotado',
'422' => 'Entidad no procesable',
'429' => 'Demasiadas solicitudes',
'500' => 'Error interno del servidor',
'502' => 'Puerta de enlace no válida',
'503' => 'Servicio no disponible',
'504' => 'Tiempo de espera de la puerta de enlace',
],
'copied' => [
'id' => 'ID del webhook copiado al portapapeles',
'secret' => 'Secret de firma copiado al portapapeles',
'response' => 'Cuerpo de la respuesta copiado',
'payload' => 'Payload copiado',
],
'errors' => [
'endpoint_not_allowed' => 'Este endpoint no está permitido.',
'endpoint_unreachable' => 'El endpoint no es accesible.',
'endpoint_http_status' => 'El endpoint devolvió HTTP :status.',
],
'flash' => [
'created' => 'Webhook creado.',
'updated' => 'Webhook actualizado.',
'deleted' => 'Webhook eliminado.',
'secret_rotated' => 'Secret de firma rotado.',
'replayed' => 'Evento del webhook reenviado.',
'tested' => 'Evento de prueba enviado.',
],
'mail' => [
'paused_subject' => 'Webhook pausado: :endpoint',
'paused_title' => 'Webhook pausado tras fallos repetidos',
'paused_preview' => 'Pausamos un webhook tras 5 fallos consecutivos de entrega.',
'paused_body' => 'Pausamos el webhook en :endpoint tras 5 fallos consecutivos de entrega. Revisa el endpoint y actívalo de nuevo en la página de detalles del webhook.',
'paused_cta' => 'Ver webhook',
],
];

View file

@ -62,7 +62,6 @@
'delay' => 'Délai', 'delay' => 'Délai',
'condition' => 'Condition', 'condition' => 'Condition',
'publish' => 'Publier', 'publish' => 'Publier',
'webhook' => 'Webhook',
'end' => 'Fin', 'end' => 'Fin',
'fetch_rss' => 'Récupérer RSS', 'fetch_rss' => 'Récupérer RSS',
'http_request' => 'Requête HTTP', 'http_request' => 'Requête HTTP',
@ -213,7 +212,6 @@
'delay' => 'Délai', 'delay' => 'Délai',
'condition' => 'Condition', 'condition' => 'Condition',
'publish' => 'Publier', 'publish' => 'Publier',
'webhook' => 'Webhook',
'end' => 'Fin', 'end' => 'Fin',
'end_summary' => 'Arrête l\'automatisation ici', 'end_summary' => 'Arrête l\'automatisation ici',
'fetch_rss' => 'Récupérer RSS', 'fetch_rss' => 'Récupérer RSS',
@ -326,11 +324,6 @@
'scheduled_offset' => 'Décalage par rapport au déclencheur (minutes)', 'scheduled_offset' => 'Décalage par rapport au déclencheur (minutes)',
'offset_summary' => ':mode · +:offset min', 'offset_summary' => ':mode · +:offset min',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'Méthode',
'payload_template' => 'Modèle de payload (JSON)',
],
'end' => [ 'end' => [
'reason' => 'Raison (facultatif)', 'reason' => 'Raison (facultatif)',
'reason_placeholder' => 'par ex. Filtré par la condition', 'reason_placeholder' => 'par ex. Filtré par la condition',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'Le graphe de l\'automatisation contient un cycle.', 'graph_contains_cycle' => 'Le graphe de l\'automatisation contient un cycle.',
'only_failed_can_retry' => 'Seules les exécutions échouées peuvent être relancées.', 'only_failed_can_retry' => 'Seules les exécutions échouées peuvent être relancées.',
'no_generated_post' => 'Aucune publication générée trouvée pour cette exécution.', 'no_generated_post' => 'Aucune publication générée trouvée pour cette exécution.',
'webhook_server_error' => 'Erreur du serveur webhook.',
'webhook_request_failed' => 'La requête webhook n\'a pas pu être exécutée.',
'webhook_missing_url' => 'Il manque une URL au nœud webhook.',
'webhook_invalid_payload_json' => 'Le modèle de payload n\'est pas du JSON valide.',
'url_not_allowed' => 'L\'URL de la requête pointe vers une adresse privée ou inaccessible et a été bloquée.', 'url_not_allowed' => 'L\'URL de la requête pointe vers une adresse privée ou inaccessible et a été bloquée.',
'node_no_longer_exists' => 'Le nœud :node_id n\'existe plus dans l\'automatisation.', 'node_no_longer_exists' => 'Le nœud :node_id n\'existe plus dans l\'automatisation.',
'no_trigger_connection' => 'Aucun nœud connecté au nœud déclencheur.', 'no_trigger_connection' => 'Aucun nœud connecté au nœud déclencheur.',

View file

@ -16,6 +16,7 @@
'signatures' => 'Signatures', 'signatures' => 'Signatures',
'labels' => 'Étiquettes', 'labels' => 'Étiquettes',
'assets' => 'Médias', 'assets' => 'Médias',
'webhooks' => 'Webhooks',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'Langue : :name', 'language' => 'Langue : :name',

139
lang/fr/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'Recevez des notifications en temps réel lorsque des publications sont créées, programmées, déprogrammées, publiées ou échouent.',
'new' => 'Créer un webhook',
'empty_title' => 'Aucun webhook pour le moment',
'empty_description' => 'Créez un webhook pour recevoir des notifications d\'événements en temps réel.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'Écoute',
'status' => 'Statut',
'last_sent' => 'Dernier envoi',
],
'events_count' => '{1} :count événement|[2,*] :count événements',
'never' => 'Jamais',
'status' => [
'enabled' => 'Actif',
'disabled' => 'Désactivé',
'paused' => 'En pause',
],
'actions' => [
'view' => 'Voir les détails',
'copy_id' => 'Copier l\'ID du webhook',
'delete' => 'Supprimer',
'edit' => 'Modifier l\'endpoint',
'enable' => 'Activer l\'endpoint',
'disable' => 'Désactiver l\'endpoint',
'rotate' => 'Faire tourner le secret de signature',
'send_test' => 'Envoyer un événement de test',
'replay' => 'Renvoyer',
'reveal_secret' => 'Afficher le secret',
'hide_secret' => 'Masquer le secret',
'copy_secret' => 'Copier le secret',
],
'create' => [
'title' => 'Créer un webhook',
'description' => 'Configurez un endpoint pour recevoir les notifications de webhook.',
'endpoint' => 'URL de l\'endpoint',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'Événements',
'events_placeholder' => 'Sélectionner des événements...',
'events_selected' => '{1} :count événement sélectionné|[2,*] :count événements sélectionnés',
'search_events' => 'Rechercher des événements...',
'no_events' => 'Aucun événement trouvé',
'submit' => 'Créer un webhook',
'cancel' => 'Annuler',
],
'edit' => [
'title' => 'Modifier l\'endpoint',
'description' => 'Mettez à jour l\'URL de l\'endpoint et les événements à écouter.',
'submit' => 'Enregistrer les modifications',
'cancel' => 'Annuler',
],
'delete' => [
'title' => 'Supprimer le webhook',
'description' => 'Voulez-vous vraiment supprimer ce webhook ? Vous ne recevrez plus de notifications d\'événements sur cet endpoint.',
'confirm' => 'Supprimer le webhook',
'cancel' => 'Annuler',
],
'rotate' => [
'title' => 'Faire tourner le secret de signature',
'description' => 'Cela génère un nouveau secret de signature. Le secret actuel cesse de fonctionner immédiatement. Mettez à jour votre endpoint pour utiliser le nouveau secret.',
'submit' => 'Faire tourner le secret',
'cancel' => 'Annuler',
],
'show' => [
'signing_secret' => 'Secret de signature',
'last_sent' => 'Dernier envoi :time',
'listening_for' => 'Écoute',
'http_status' => 'Statut HTTP',
'status_code' => ':code - :reason',
'attempts' => 'Tentatives',
'delivered_at' => 'Livré le',
'response_body' => 'Corps de la réponse',
'response' => 'Réponse',
'no_response_body' => 'Aucun corps de réponse',
'no_response' => 'Aucune réponse',
'payload' => 'Payload du message',
'empty_title' => 'Aucun événement pour le moment',
'empty_description' => 'Lorsque des publications sont créées, programmées, déprogrammées ou publiées, les événements du webhook apparaissent ici.',
],
'events' => [
'group_posts' => 'Publications',
'post_created' => 'Publication créée',
'post_scheduled' => 'Publication programmée',
'post_unscheduled' => 'Publication déprogrammée',
'post_published' => 'Publication publiée',
'post_partially_published' => 'Publication partiellement publiée',
'post_failed' => 'Échec de publication',
'post_deleted' => 'Publication supprimée',
],
'http_reasons' => [
'unknown' => 'Inconnu',
'200' => 'OK',
'201' => 'Créé',
'202' => 'Accepté',
'204' => 'Aucun contenu',
'400' => 'Requête incorrecte',
'401' => 'Non autorisé',
'403' => 'Interdit',
'404' => 'Introuvable',
'408' => 'Délai d\'attente dépassé',
'422' => 'Entité non traitable',
'429' => 'Trop de requêtes',
'500' => 'Erreur interne du serveur',
'502' => 'Passerelle invalide',
'503' => 'Service indisponible',
'504' => 'Délai d\'attente de la passerelle',
],
'copied' => [
'id' => 'ID du webhook copié dans le presse-papiers',
'secret' => 'Secret de signature copié dans le presse-papiers',
'response' => 'Corps de la réponse copié',
'payload' => 'Payload copié',
],
'errors' => [
'endpoint_not_allowed' => 'Cet endpoint n\'est pas autorisé.',
'endpoint_unreachable' => 'L\'endpoint n\'est pas joignable.',
'endpoint_http_status' => 'L\'endpoint a renvoyé HTTP :status.',
],
'flash' => [
'created' => 'Webhook créé.',
'updated' => 'Webhook mis à jour.',
'deleted' => 'Webhook supprimé.',
'secret_rotated' => 'Secret de signature renouvelé.',
'replayed' => 'Événement du webhook renvoyé.',
'tested' => 'Événement de test envoyé.',
],
'mail' => [
'paused_subject' => 'Webhook en pause : :endpoint',
'paused_title' => 'Webhook mis en pause après des échecs répétés',
'paused_preview' => 'Nous avons mis un webhook en pause après 5 échecs de livraison consécutifs.',
'paused_body' => 'Nous avons mis le webhook de :endpoint en pause après 5 échecs de livraison consécutifs. Vérifiez l\'endpoint et réactivez-le depuis la page de détails du webhook.',
'paused_cta' => 'Voir le webhook',
],
];

View file

@ -62,7 +62,6 @@
'delay' => 'Ritardo', 'delay' => 'Ritardo',
'condition' => 'Condizione', 'condition' => 'Condizione',
'publish' => 'Pubblica', 'publish' => 'Pubblica',
'webhook' => 'Webhook',
'end' => 'Fine', 'end' => 'Fine',
'fetch_rss' => 'Recupera RSS', 'fetch_rss' => 'Recupera RSS',
'http_request' => 'Richiesta HTTP', 'http_request' => 'Richiesta HTTP',
@ -213,7 +212,6 @@
'delay' => 'Ritardo', 'delay' => 'Ritardo',
'condition' => 'Condizione', 'condition' => 'Condizione',
'publish' => 'Pubblica', 'publish' => 'Pubblica',
'webhook' => 'Webhook',
'end' => 'Fine', 'end' => 'Fine',
'end_summary' => 'Interrompe l\'automazione qui', 'end_summary' => 'Interrompe l\'automazione qui',
'fetch_rss' => 'Recupera RSS', 'fetch_rss' => 'Recupera RSS',
@ -326,11 +324,6 @@
'scheduled_offset' => 'Scostamento dal trigger (minuti)', 'scheduled_offset' => 'Scostamento dal trigger (minuti)',
'offset_summary' => ':mode · +:offset min', 'offset_summary' => ':mode · +:offset min',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'Metodo',
'payload_template' => 'Modello di payload (JSON)',
],
'end' => [ 'end' => [
'reason' => 'Motivo (facoltativo)', 'reason' => 'Motivo (facoltativo)',
'reason_placeholder' => 'es. Escluso dalla condizione', 'reason_placeholder' => 'es. Escluso dalla condizione',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'Il grafo dell\'automazione contiene un ciclo.', 'graph_contains_cycle' => 'Il grafo dell\'automazione contiene un ciclo.',
'only_failed_can_retry' => 'Solo le esecuzioni non riuscite possono essere ritentate.', 'only_failed_can_retry' => 'Solo le esecuzioni non riuscite possono essere ritentate.',
'no_generated_post' => 'Nessun post generato trovato nell\'esecuzione.', 'no_generated_post' => 'Nessun post generato trovato nell\'esecuzione.',
'webhook_server_error' => 'Errore del server webhook.',
'webhook_request_failed' => 'Impossibile completare la richiesta webhook.',
'webhook_missing_url' => 'Al nodo webhook manca un URL.',
'webhook_invalid_payload_json' => 'Il modello di payload non è un JSON valido.',
'url_not_allowed' => 'L\'URL della richiesta punta a un indirizzo privato o irraggiungibile ed è stato bloccato.', 'url_not_allowed' => 'L\'URL della richiesta punta a un indirizzo privato o irraggiungibile ed è stato bloccato.',
'node_no_longer_exists' => 'Il nodo :node_id non esiste più nell\'automazione.', 'node_no_longer_exists' => 'Il nodo :node_id non esiste più nell\'automazione.',
'no_trigger_connection' => 'Nessun nodo collegato al nodo Trigger.', 'no_trigger_connection' => 'Nessun nodo collegato al nodo Trigger.',

View file

@ -16,6 +16,7 @@
'signatures' => 'Firme', 'signatures' => 'Firme',
'labels' => 'Etichette', 'labels' => 'Etichette',
'assets' => 'Risorse', 'assets' => 'Risorse',
'webhooks' => 'Webhook',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'Lingua: :name', 'language' => 'Lingua: :name',

139
lang/it/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'Ricevi notifiche in tempo reale quando i post vengono creati, programmati, deprogrammati, pubblicati o falliscono.',
'new' => 'Crea webhook',
'empty_title' => 'Nessun webhook ancora',
'empty_description' => 'Crea un webhook per ricevere notifiche di eventi in tempo reale.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'In ascolto',
'status' => 'Stato',
'last_sent' => 'Ultimo invio',
],
'events_count' => '{1} :count evento|[2,*] :count eventi',
'never' => 'Mai',
'status' => [
'enabled' => 'Attivo',
'disabled' => 'Disattivato',
'paused' => 'In pausa',
],
'actions' => [
'view' => 'Vedi dettagli',
'copy_id' => 'Copia ID del webhook',
'delete' => 'Elimina',
'edit' => 'Modifica endpoint',
'enable' => 'Attiva endpoint',
'disable' => 'Disattiva endpoint',
'rotate' => 'Ruota secret di firma',
'send_test' => 'Invia evento di test',
'replay' => 'Reinvia',
'reveal_secret' => 'Mostra secret',
'hide_secret' => 'Nascondi secret',
'copy_secret' => 'Copia secret',
],
'create' => [
'title' => 'Crea webhook',
'description' => 'Configura un endpoint per ricevere le notifiche webhook.',
'endpoint' => 'URL dell\'endpoint',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'Eventi',
'events_placeholder' => 'Seleziona eventi...',
'events_selected' => '{1} :count evento selezionato|[2,*] :count eventi selezionati',
'search_events' => 'Cerca eventi...',
'no_events' => 'Nessun evento trovato',
'submit' => 'Crea webhook',
'cancel' => 'Annulla',
],
'edit' => [
'title' => 'Modifica endpoint',
'description' => 'Aggiorna l\'URL dell\'endpoint e gli eventi da ascoltare.',
'submit' => 'Salva modifiche',
'cancel' => 'Annulla',
],
'delete' => [
'title' => 'Elimina webhook',
'description' => 'Vuoi davvero eliminare questo webhook? Non riceverai più notifiche di eventi su questo endpoint.',
'confirm' => 'Elimina webhook',
'cancel' => 'Annulla',
],
'rotate' => [
'title' => 'Ruota secret di firma',
'description' => 'Questo genera un nuovo secret di firma. Il secret attuale smette di funzionare subito. Aggiorna il tuo endpoint per usare il nuovo secret.',
'submit' => 'Ruota secret',
'cancel' => 'Annulla',
],
'show' => [
'signing_secret' => 'Secret di firma',
'last_sent' => 'Ultimo invio :time',
'listening_for' => 'In ascolto',
'http_status' => 'Stato HTTP',
'status_code' => ':code - :reason',
'attempts' => 'Tentativi',
'delivered_at' => 'Consegnato il',
'response_body' => 'Corpo della risposta',
'response' => 'Risposta',
'no_response_body' => 'Nessun corpo della risposta',
'no_response' => 'Nessuna risposta',
'payload' => 'Payload del messaggio',
'empty_title' => 'Nessun evento ancora',
'empty_description' => 'Quando i post vengono creati, programmati, deprogrammati o pubblicati, gli eventi del webhook appaiono qui.',
],
'events' => [
'group_posts' => 'Post',
'post_created' => 'Post creato',
'post_scheduled' => 'Post programmato',
'post_unscheduled' => 'Programmazione annullata',
'post_published' => 'Post pubblicato',
'post_partially_published' => 'Post pubblicato parzialmente',
'post_failed' => 'Post non riuscito',
'post_deleted' => 'Post eliminato',
],
'http_reasons' => [
'unknown' => 'Sconosciuto',
'200' => 'OK',
'201' => 'Creato',
'202' => 'Accettato',
'204' => 'Nessun contenuto',
'400' => 'Richiesta non valida',
'401' => 'Non autorizzato',
'403' => 'Vietato',
'404' => 'Non trovato',
'408' => 'Tempo scaduto',
'422' => 'Entità non elaborabile',
'429' => 'Troppe richieste',
'500' => 'Errore interno del server',
'502' => 'Gateway non valido',
'503' => 'Servizio non disponibile',
'504' => 'Tempo scaduto del gateway',
],
'copied' => [
'id' => 'ID del webhook copiato negli appunti',
'secret' => 'Secret di firma copiato negli appunti',
'response' => 'Corpo della risposta copiato',
'payload' => 'Payload copiato',
],
'errors' => [
'endpoint_not_allowed' => 'Questo endpoint non è consentito.',
'endpoint_unreachable' => 'L\'endpoint non è raggiungibile.',
'endpoint_http_status' => 'L\'endpoint ha restituito HTTP :status.',
],
'flash' => [
'created' => 'Webhook creato.',
'updated' => 'Webhook aggiornato.',
'deleted' => 'Webhook eliminato.',
'secret_rotated' => 'Secret di firma ruotato.',
'replayed' => 'Evento del webhook reinviato.',
'tested' => 'Evento di test inviato.',
],
'mail' => [
'paused_subject' => 'Webhook in pausa: :endpoint',
'paused_title' => 'Webhook messo in pausa dopo errori ripetuti',
'paused_preview' => 'Abbiamo messo in pausa un webhook dopo 5 errori di consegna consecutivi.',
'paused_body' => 'Abbiamo messo in pausa il webhook su :endpoint dopo 5 errori di consegna consecutivi. Controlla l\'endpoint e riattivalo dalla pagina dei dettagli del webhook.',
'paused_cta' => 'Vedi webhook',
],
];

View file

@ -62,7 +62,6 @@
'delay' => '遅延', 'delay' => '遅延',
'condition' => '条件', 'condition' => '条件',
'publish' => '公開', 'publish' => '公開',
'webhook' => 'Webhook',
'end' => '終了', 'end' => '終了',
'fetch_rss' => 'RSS を取得', 'fetch_rss' => 'RSS を取得',
'http_request' => 'HTTP リクエスト', 'http_request' => 'HTTP リクエスト',
@ -213,7 +212,6 @@
'delay' => '遅延', 'delay' => '遅延',
'condition' => '条件', 'condition' => '条件',
'publish' => '公開', 'publish' => '公開',
'webhook' => 'Webhook',
'end' => '終了', 'end' => '終了',
'end_summary' => 'ここでオートメーションを停止します', 'end_summary' => 'ここでオートメーションを停止します',
'fetch_rss' => 'RSS を取得', 'fetch_rss' => 'RSS を取得',
@ -326,11 +324,6 @@
'scheduled_offset' => 'トリガーからのオフセット(分)', 'scheduled_offset' => 'トリガーからのオフセット(分)',
'offset_summary' => ':mode · +:offset 分', 'offset_summary' => ':mode · +:offset 分',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'メソッド',
'payload_template' => 'ペイロードテンプレートJSON',
],
'end' => [ 'end' => [
'reason' => '理由(任意)', 'reason' => '理由(任意)',
'reason_placeholder' => '例: 条件により除外', 'reason_placeholder' => '例: 条件により除外',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'オートメーションのグラフに循環が含まれています。', 'graph_contains_cycle' => 'オートメーションのグラフに循環が含まれています。',
'only_failed_can_retry' => '失敗した実行のみ再試行できます。', 'only_failed_can_retry' => '失敗した実行のみ再試行できます。',
'no_generated_post' => '実行で生成された投稿が見つかりません。', 'no_generated_post' => '実行で生成された投稿が見つかりません。',
'webhook_server_error' => 'Webhook サーバーエラー。',
'webhook_request_failed' => 'Webhook リクエストを完了できませんでした。',
'webhook_missing_url' => 'Webhook ノードに URL がありません。',
'webhook_invalid_payload_json' => 'ペイロードテンプレートが有効な JSON ではありません。',
'url_not_allowed' => 'リクエスト URL がプライベートまたは到達不能なアドレスを指しているためブロックされました。', 'url_not_allowed' => 'リクエスト URL がプライベートまたは到達不能なアドレスを指しているためブロックされました。',
'node_no_longer_exists' => 'ノード :node_id はオートメーションに存在しなくなりました。', 'node_no_longer_exists' => 'ノード :node_id はオートメーションに存在しなくなりました。',
'no_trigger_connection' => 'トリガーノードに接続されたノードがありません。', 'no_trigger_connection' => 'トリガーノードに接続されたノードがありません。',

View file

@ -16,6 +16,7 @@
'signatures' => '署名', 'signatures' => '署名',
'labels' => 'ラベル', 'labels' => 'ラベル',
'assets' => 'アセット', 'assets' => 'アセット',
'webhooks' => 'ウェブフック',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => '言語: :name', 'language' => '言語: :name',

139
lang/ja/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => '投稿の作成、予約、予約解除、公開、失敗をリアルタイムで通知します。',
'new' => 'Webhookを作成',
'empty_title' => 'Webhookはまだありません',
'empty_description' => 'Webhookを作成すると、イベント通知をリアルタイムで受け取れます。',
'table' => [
'endpoint' => 'Endpoint',
'events' => '購読中',
'status' => 'ステータス',
'last_sent' => '最終送信',
],
'events_count' => '{1} :count件のイベント|[2,*] :count件のイベント',
'never' => 'なし',
'status' => [
'enabled' => '有効',
'disabled' => '無効',
'paused' => '一時停止中',
],
'actions' => [
'view' => '詳細を見る',
'copy_id' => 'Webhook IDをコピー',
'delete' => '削除',
'edit' => 'Endpointを編集',
'enable' => 'Endpointを有効化',
'disable' => 'Endpointを無効化',
'rotate' => '署名シークレットを更新',
'send_test' => 'テストイベントを送信',
'replay' => '再送信',
'reveal_secret' => 'シークレットを表示',
'hide_secret' => 'シークレットを隠す',
'copy_secret' => 'シークレットをコピー',
],
'create' => [
'title' => 'Webhookを作成',
'description' => '通知を受け取るEndpointを設定します。',
'endpoint' => 'Endpoint URL',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'イベント',
'events_placeholder' => 'イベントを選択...',
'events_selected' => '{1} :count件のイベントを選択|[2,*] :count件のイベントを選択',
'search_events' => 'イベントを検索...',
'no_events' => 'イベントが見つかりません',
'submit' => 'Webhookを作成',
'cancel' => 'キャンセル',
],
'edit' => [
'title' => 'Endpointを編集',
'description' => 'Endpoint URLと購読するイベントを更新します。',
'submit' => '変更を保存',
'cancel' => 'キャンセル',
],
'delete' => [
'title' => 'Webhookを削除',
'description' => 'このWebhookを削除しますかこのEndpointへのイベント通知は届かなくなります。',
'confirm' => 'Webhookを削除',
'cancel' => 'キャンセル',
],
'rotate' => [
'title' => '署名シークレットを更新',
'description' => '新しい署名シークレットを発行します。現在のシークレットはすぐに使えなくなります。Endpoint側も新しいシークレットに更新してください。',
'submit' => 'シークレットを更新',
'cancel' => 'キャンセル',
],
'show' => [
'signing_secret' => '署名シークレット',
'last_sent' => '最終送信 :time',
'listening_for' => '購読中',
'http_status' => 'HTTPステータス',
'status_code' => ':code - :reason',
'attempts' => '試行回数',
'delivered_at' => '配信日時',
'response_body' => 'レスポンス本文',
'response' => 'レスポンス',
'no_response_body' => 'レスポンス本文なし',
'no_response' => 'レスポンスなし',
'payload' => 'メッセージのペイロード',
'empty_title' => 'イベントはまだありません',
'empty_description' => '投稿が作成、予約、予約解除、公開されると、Webhookイベントがここに表示されます。',
],
'events' => [
'group_posts' => '投稿',
'post_created' => '投稿を作成',
'post_scheduled' => '投稿を予約',
'post_unscheduled' => '予約を解除',
'post_published' => '投稿を公開',
'post_partially_published' => '一部公開',
'post_failed' => '投稿に失敗',
'post_deleted' => '投稿を削除',
],
'http_reasons' => [
'unknown' => '不明',
'200' => 'OK',
'201' => '作成',
'202' => '受理',
'204' => 'コンテンツなし',
'400' => '不正なリクエスト',
'401' => '認証が必要',
'403' => '禁止',
'404' => '見つかりません',
'408' => 'リクエストタイムアウト',
'422' => '処理できないエンティティ',
'429' => 'リクエスト過多',
'500' => 'サーバー内部エラー',
'502' => '不正なゲートウェイ',
'503' => 'サービス利用不可',
'504' => 'ゲートウェイタイムアウト',
],
'copied' => [
'id' => 'Webhook IDをクリップボードにコピーしました',
'secret' => '署名シークレットをクリップボードにコピーしました',
'response' => 'レスポンス本文をコピーしました',
'payload' => 'ペイロードをコピーしました',
],
'errors' => [
'endpoint_not_allowed' => 'このEndpointは許可されていません。',
'endpoint_unreachable' => 'Endpointに到達できません。',
'endpoint_http_status' => 'EndpointがHTTP :statusを返しました。',
],
'flash' => [
'created' => 'Webhookを作成しました。',
'updated' => 'Webhookを更新しました。',
'deleted' => 'Webhookを削除しました。',
'secret_rotated' => '署名シークレットを更新しました。',
'replayed' => 'Webhookイベントを再送信しました。',
'tested' => 'テストイベントを送信しました。',
],
'mail' => [
'paused_subject' => 'Webhookを一時停止しました: :endpoint',
'paused_title' => '連続した失敗のためWebhookを一時停止しました',
'paused_preview' => '配信が5回連続で失敗したため、Webhookを一時停止しました。',
'paused_body' => ':endpoint のWebhookを、配信が5回連続で失敗したため一時停止しました。Endpointを確認し、Webhookの詳細ページから再度有効にしてください。',
'paused_cta' => 'Webhookを見る',
],
];

View file

@ -62,7 +62,6 @@
'delay' => '지연', 'delay' => '지연',
'condition' => '조건', 'condition' => '조건',
'publish' => '게시', 'publish' => '게시',
'webhook' => '웹훅',
'end' => '종료', 'end' => '종료',
'fetch_rss' => 'RSS 가져오기', 'fetch_rss' => 'RSS 가져오기',
'http_request' => 'HTTP 요청', 'http_request' => 'HTTP 요청',
@ -213,7 +212,6 @@
'delay' => '지연', 'delay' => '지연',
'condition' => '조건', 'condition' => '조건',
'publish' => '게시', 'publish' => '게시',
'webhook' => '웹훅',
'end' => '종료', 'end' => '종료',
'end_summary' => '여기서 자동화를 중지합니다', 'end_summary' => '여기서 자동화를 중지합니다',
'fetch_rss' => 'RSS 가져오기', 'fetch_rss' => 'RSS 가져오기',
@ -326,11 +324,6 @@
'scheduled_offset' => '트리거 기준 오프셋 (분)', 'scheduled_offset' => '트리거 기준 오프셋 (분)',
'offset_summary' => ':mode · +:offset분', 'offset_summary' => ':mode · +:offset분',
], ],
'webhook' => [
'url' => 'URL',
'method' => '메서드',
'payload_template' => '페이로드 템플릿 (JSON)',
],
'end' => [ 'end' => [
'reason' => '사유 (선택)', 'reason' => '사유 (선택)',
'reason_placeholder' => '예: 조건에 의해 필터링됨', 'reason_placeholder' => '예: 조건에 의해 필터링됨',
@ -394,10 +387,6 @@
'graph_contains_cycle' => '자동화 그래프에 순환이 포함되어 있습니다.', 'graph_contains_cycle' => '자동화 그래프에 순환이 포함되어 있습니다.',
'only_failed_can_retry' => '실패한 실행만 재시도할 수 있습니다.', 'only_failed_can_retry' => '실패한 실행만 재시도할 수 있습니다.',
'no_generated_post' => '실행에서 생성된 게시물을 찾을 수 없습니다.', 'no_generated_post' => '실행에서 생성된 게시물을 찾을 수 없습니다.',
'webhook_server_error' => '웹훅 서버 오류.',
'webhook_request_failed' => '웹훅 요청을 완료할 수 없습니다.',
'webhook_missing_url' => '웹훅 노드에 URL이 없습니다.',
'webhook_invalid_payload_json' => '페이로드 템플릿이 유효한 JSON이 아닙니다.',
'url_not_allowed' => '요청 URL이 비공개이거나 접근할 수 없는 주소를 가리켜 차단되었습니다.', 'url_not_allowed' => '요청 URL이 비공개이거나 접근할 수 없는 주소를 가리켜 차단되었습니다.',
'node_no_longer_exists' => '노드 :node_id이(가) 자동화에 더 이상 존재하지 않습니다.', 'node_no_longer_exists' => '노드 :node_id이(가) 자동화에 더 이상 존재하지 않습니다.',
'no_trigger_connection' => '트리거 노드에 연결된 노드가 없습니다.', 'no_trigger_connection' => '트리거 노드에 연결된 노드가 없습니다.',

View file

@ -16,6 +16,7 @@
'signatures' => '서명', 'signatures' => '서명',
'labels' => '라벨', 'labels' => '라벨',
'assets' => '에셋', 'assets' => '에셋',
'webhooks' => '웹훅',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => '언어: :name', 'language' => '언어: :name',

139
lang/ko/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => '게시물이 생성되거나, 예약되거나, 예약이 취소되거나, 게시되거나, 실패하면 실시간으로 알림을 받습니다.',
'new' => '웹훅 만들기',
'empty_title' => '아직 웹훅이 없습니다',
'empty_description' => '웹훅을 만들어 이벤트 알림을 실시간으로 받으세요.',
'table' => [
'endpoint' => 'Endpoint',
'events' => '수신 중',
'status' => '상태',
'last_sent' => '마지막 전송',
],
'events_count' => '{1} 이벤트 :count개|[2,*] 이벤트 :count개',
'never' => '없음',
'status' => [
'enabled' => '사용 중',
'disabled' => '사용 안 함',
'paused' => '일시정지됨',
],
'actions' => [
'view' => '세부정보 보기',
'copy_id' => '웹훅 ID 복사',
'delete' => '삭제',
'edit' => 'Endpoint 편집',
'enable' => 'Endpoint 사용',
'disable' => 'Endpoint 사용 안 함',
'rotate' => '서명 시크릿 교체',
'send_test' => '테스트 이벤트 보내기',
'replay' => '다시 보내기',
'reveal_secret' => '시크릿 표시',
'hide_secret' => '시크릿 숨기기',
'copy_secret' => '시크릿 복사',
],
'create' => [
'title' => '웹훅 만들기',
'description' => '웹훅 알림을 받을 endpoint를 설정하세요.',
'endpoint' => 'Endpoint URL',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => '이벤트',
'events_placeholder' => '이벤트 선택...',
'events_selected' => '{1} 이벤트 :count개 선택됨|[2,*] 이벤트 :count개 선택됨',
'search_events' => '이벤트 검색...',
'no_events' => '이벤트를 찾을 수 없습니다',
'submit' => '웹훅 만들기',
'cancel' => '취소',
],
'edit' => [
'title' => 'Endpoint 편집',
'description' => 'Endpoint URL과 수신할 이벤트를 업데이트하세요.',
'submit' => '변경 사항 저장',
'cancel' => '취소',
],
'delete' => [
'title' => '웹훅 삭제',
'description' => '이 웹훅을 삭제할까요? 이 endpoint로는 더 이상 이벤트 알림을 받지 않습니다.',
'confirm' => '웹훅 삭제',
'cancel' => '취소',
],
'rotate' => [
'title' => '서명 시크릿 교체',
'description' => '새 서명 시크릿이 생성됩니다. 현재 시크릿은 바로 작동을 멈춥니다. Endpoint가 새 시크릿을 쓰도록 업데이트하세요.',
'submit' => '시크릿 교체',
'cancel' => '취소',
],
'show' => [
'signing_secret' => '서명 시크릿',
'last_sent' => '마지막 전송 :time',
'listening_for' => '수신 중',
'http_status' => 'HTTP 상태',
'status_code' => ':code - :reason',
'attempts' => '시도',
'delivered_at' => '전달됨',
'response_body' => '응답 본문',
'response' => '응답',
'no_response_body' => '응답 본문 없음',
'no_response' => '응답 없음',
'payload' => '메시지 페이로드',
'empty_title' => '아직 웹훅 이벤트가 없습니다',
'empty_description' => '게시물이 생성되거나, 예약되거나, 예약이 취소되거나, 게시되면 웹훅 이벤트가 여기에 나타납니다.',
],
'events' => [
'group_posts' => '게시물',
'post_created' => '게시물 생성됨',
'post_scheduled' => '게시물 예약됨',
'post_unscheduled' => '예약 취소됨',
'post_published' => '게시물 게시됨',
'post_partially_published' => '부분 게시됨',
'post_failed' => '게시 실패',
'post_deleted' => '게시물 삭제됨',
],
'http_reasons' => [
'unknown' => '알 수 없음',
'200' => 'OK',
'201' => '생성됨',
'202' => '수락됨',
'204' => '콘텐츠 없음',
'400' => '잘못된 요청',
'401' => '인증되지 않음',
'403' => '금지됨',
'404' => '찾을 수 없음',
'408' => '요청 시간 초과',
'422' => '처리할 수 없는 엔터티',
'429' => '요청이 너무 많음',
'500' => '내부 서버 오류',
'502' => '잘못된 게이트웨이',
'503' => '서비스를 사용할 수 없음',
'504' => '게이트웨이 시간 초과',
],
'copied' => [
'id' => '웹훅 ID를 클립보드에 복사했습니다',
'secret' => '서명 시크릿을 클립보드에 복사했습니다',
'response' => '응답 본문을 복사했습니다',
'payload' => '페이로드를 복사했습니다',
],
'errors' => [
'endpoint_not_allowed' => '이 endpoint는 허용되지 않습니다.',
'endpoint_unreachable' => 'endpoint에 연결할 수 없습니다.',
'endpoint_http_status' => 'endpoint가 HTTP :status를 반환했습니다.',
],
'flash' => [
'created' => '웹훅을 만들었습니다.',
'updated' => '웹훅을 업데이트했습니다.',
'deleted' => '웹훅을 삭제했습니다.',
'secret_rotated' => '서명 시크릿을 교체했습니다.',
'replayed' => '웹훅 이벤트를 다시 보냈습니다.',
'tested' => '테스트 이벤트를 보냈습니다.',
],
'mail' => [
'paused_subject' => '웹훅이 일시정지됨: :endpoint',
'paused_title' => '반복된 실패로 웹훅이 일시정지되었습니다',
'paused_preview' => '연속 5회 전달에 실패한 뒤 웹훅을 일시정지했습니다.',
'paused_body' => ':endpoint의 웹훅을 연속 5회 전달 실패 후 일시정지했습니다. Endpoint를 확인한 뒤 웹훅 상세 페이지에서 다시 사용하세요.',
'paused_cta' => '웹훅 보기',
],
];

View file

@ -62,7 +62,6 @@
'delay' => 'Vertraging', 'delay' => 'Vertraging',
'condition' => 'Voorwaarde', 'condition' => 'Voorwaarde',
'publish' => 'Publiceren', 'publish' => 'Publiceren',
'webhook' => 'Webhook',
'end' => 'Einde', 'end' => 'Einde',
'fetch_rss' => 'RSS ophalen', 'fetch_rss' => 'RSS ophalen',
'http_request' => 'HTTP-verzoek', 'http_request' => 'HTTP-verzoek',
@ -213,7 +212,6 @@
'delay' => 'Vertraging', 'delay' => 'Vertraging',
'condition' => 'Voorwaarde', 'condition' => 'Voorwaarde',
'publish' => 'Publiceren', 'publish' => 'Publiceren',
'webhook' => 'Webhook',
'end' => 'Einde', 'end' => 'Einde',
'end_summary' => 'Stopt de automatisering hier', 'end_summary' => 'Stopt de automatisering hier',
'fetch_rss' => 'RSS ophalen', 'fetch_rss' => 'RSS ophalen',
@ -326,11 +324,6 @@
'scheduled_offset' => 'Verschuiving vanaf trigger (minuten)', 'scheduled_offset' => 'Verschuiving vanaf trigger (minuten)',
'offset_summary' => ':mode · +:offset min', 'offset_summary' => ':mode · +:offset min',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'Methode',
'payload_template' => 'Payload-sjabloon (JSON)',
],
'end' => [ 'end' => [
'reason' => 'Reden (optioneel)', 'reason' => 'Reden (optioneel)',
'reason_placeholder' => 'bijv. Uitgefilterd door voorwaarde', 'reason_placeholder' => 'bijv. Uitgefilterd door voorwaarde',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'De automatiseringsgraaf bevat een cyclus.', 'graph_contains_cycle' => 'De automatiseringsgraaf bevat een cyclus.',
'only_failed_can_retry' => 'Alleen mislukte uitvoeringen kunnen opnieuw worden geprobeerd.', 'only_failed_can_retry' => 'Alleen mislukte uitvoeringen kunnen opnieuw worden geprobeerd.',
'no_generated_post' => 'Geen gegenereerde post gevonden bij de uitvoering.', 'no_generated_post' => 'Geen gegenereerde post gevonden bij de uitvoering.',
'webhook_server_error' => 'Serverfout bij de webhook.',
'webhook_request_failed' => 'Het webhookverzoek kon niet worden voltooid.',
'webhook_missing_url' => 'De webhooknode mist een URL.',
'webhook_invalid_payload_json' => 'Het payload-sjabloon is geen geldige JSON.',
'url_not_allowed' => 'De verzoek-URL verwijst naar een privé of onbereikbaar adres en is geblokkeerd.', 'url_not_allowed' => 'De verzoek-URL verwijst naar een privé of onbereikbaar adres en is geblokkeerd.',
'node_no_longer_exists' => 'Node :node_id bestaat niet meer in de automatisering.', 'node_no_longer_exists' => 'Node :node_id bestaat niet meer in de automatisering.',
'no_trigger_connection' => 'Geen node verbonden met de triggernode.', 'no_trigger_connection' => 'Geen node verbonden met de triggernode.',

View file

@ -16,6 +16,7 @@
'signatures' => 'Handtekeningen', 'signatures' => 'Handtekeningen',
'labels' => 'Labels', 'labels' => 'Labels',
'assets' => 'Assets', 'assets' => 'Assets',
'webhooks' => 'Webhooks',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'Taal: :name', 'language' => 'Taal: :name',

139
lang/nl/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'Ontvang realtime meldingen wanneer posts worden aangemaakt, gepland, ontpland, gepubliceerd of mislukken.',
'new' => 'Webhook maken',
'empty_title' => 'Nog geen webhooks',
'empty_description' => 'Maak een webhook om realtime gebeurtenismeldingen te ontvangen.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'Luistert naar',
'status' => 'Status',
'last_sent' => 'Laatst verzonden',
],
'events_count' => '{1} :count gebeurtenis|[2,*] :count gebeurtenissen',
'never' => 'Nooit',
'status' => [
'enabled' => 'Actief',
'disabled' => 'Uitgeschakeld',
'paused' => 'Gepauzeerd',
],
'actions' => [
'view' => 'Details bekijken',
'copy_id' => 'Webhook-ID kopiëren',
'delete' => 'Verwijderen',
'edit' => 'Endpoint bewerken',
'enable' => 'Endpoint inschakelen',
'disable' => 'Endpoint uitschakelen',
'rotate' => 'Ondertekeningssecret roteren',
'send_test' => 'Testgebeurtenis versturen',
'replay' => 'Opnieuw versturen',
'reveal_secret' => 'Secret tonen',
'hide_secret' => 'Secret verbergen',
'copy_secret' => 'Secret kopiëren',
],
'create' => [
'title' => 'Webhook maken',
'description' => 'Stel een endpoint in om webhookmeldingen te ontvangen.',
'endpoint' => 'Endpoint-URL',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'Gebeurtenissen',
'events_placeholder' => 'Gebeurtenissen selecteren...',
'events_selected' => '{1} :count gebeurtenis geselecteerd|[2,*] :count gebeurtenissen geselecteerd',
'search_events' => 'Gebeurtenissen zoeken...',
'no_events' => 'Geen gebeurtenissen gevonden',
'submit' => 'Webhook maken',
'cancel' => 'Annuleren',
],
'edit' => [
'title' => 'Endpoint bewerken',
'description' => 'Werk de endpoint-URL en de gebeurtenissen om naar te luisteren bij.',
'submit' => 'Wijzigingen opslaan',
'cancel' => 'Annuleren',
],
'delete' => [
'title' => 'Webhook verwijderen',
'description' => 'Weet je zeker dat je deze webhook wilt verwijderen? Je ontvangt geen gebeurtenismeldingen meer op dit endpoint.',
'confirm' => 'Webhook verwijderen',
'cancel' => 'Annuleren',
],
'rotate' => [
'title' => 'Ondertekeningssecret roteren',
'description' => 'Dit genereert een nieuw ondertekeningssecret. Het huidige secret stopt meteen met werken. Werk je endpoint bij om het nieuwe secret te gebruiken.',
'submit' => 'Secret roteren',
'cancel' => 'Annuleren',
],
'show' => [
'signing_secret' => 'Ondertekeningssecret',
'last_sent' => 'Laatst verzonden :time',
'listening_for' => 'Luistert naar',
'http_status' => 'HTTP-status',
'status_code' => ':code - :reason',
'attempts' => 'Pogingen',
'delivered_at' => 'Afgeleverd om',
'response_body' => 'Antwoordtekst',
'response' => 'Antwoord',
'no_response_body' => 'Geen antwoordtekst',
'no_response' => 'Geen antwoord',
'payload' => 'Berichtpayload',
'empty_title' => 'Nog geen webhookgebeurtenissen',
'empty_description' => 'Zodra posts worden aangemaakt, gepland, ontpland of gepubliceerd, zie je de webhookgebeurtenissen hier.',
],
'events' => [
'group_posts' => 'Posts',
'post_created' => 'Post aangemaakt',
'post_scheduled' => 'Post gepland',
'post_unscheduled' => 'Planning opgeheven',
'post_published' => 'Post gepubliceerd',
'post_partially_published' => 'Post gedeeltelijk gepubliceerd',
'post_failed' => 'Post mislukt',
'post_deleted' => 'Post verwijderd',
],
'http_reasons' => [
'unknown' => 'Onbekend',
'200' => 'OK',
'201' => 'Aangemaakt',
'202' => 'Geaccepteerd',
'204' => 'Geen inhoud',
'400' => 'Ongeldige aanvraag',
'401' => 'Niet geautoriseerd',
'403' => 'Verboden',
'404' => 'Niet gevonden',
'408' => 'Time-out',
'422' => 'Niet verwerkbaar',
'429' => 'Te veel verzoeken',
'500' => 'Interne serverfout',
'502' => 'Ongeldige gateway',
'503' => 'Dienst niet beschikbaar',
'504' => 'Gateway-time-out',
],
'copied' => [
'id' => 'Webhook-ID naar klembord gekopieerd',
'secret' => 'Ondertekeningssecret naar klembord gekopieerd',
'response' => 'Antwoordtekst gekopieerd',
'payload' => 'Payload gekopieerd',
],
'errors' => [
'endpoint_not_allowed' => 'Dit endpoint is niet toegestaan.',
'endpoint_unreachable' => 'Het endpoint is niet bereikbaar.',
'endpoint_http_status' => 'Het endpoint gaf HTTP :status terug.',
],
'flash' => [
'created' => 'Webhook gemaakt.',
'updated' => 'Webhook bijgewerkt.',
'deleted' => 'Webhook verwijderd.',
'secret_rotated' => 'Ondertekeningssecret geroteerd.',
'replayed' => 'Webhookgebeurtenis opnieuw verstuurd.',
'tested' => 'Testgebeurtenis verstuurd.',
],
'mail' => [
'paused_subject' => 'Webhook gepauzeerd: :endpoint',
'paused_title' => 'Webhook gepauzeerd na herhaalde fouten',
'paused_preview' => 'We hebben een webhook gepauzeerd na 5 opeenvolgende afleverfouten.',
'paused_body' => 'We hebben de webhook op :endpoint gepauzeerd na 5 opeenvolgende afleverfouten. Controleer het endpoint en schakel het weer in op de webhookdetailpagina.',
'paused_cta' => 'Webhook bekijken',
],
];

View file

@ -62,7 +62,6 @@
'delay' => 'Opóźnienie', 'delay' => 'Opóźnienie',
'condition' => 'Warunek', 'condition' => 'Warunek',
'publish' => 'Publikuj', 'publish' => 'Publikuj',
'webhook' => 'Webhook',
'end' => 'Koniec', 'end' => 'Koniec',
'fetch_rss' => 'Pobierz RSS', 'fetch_rss' => 'Pobierz RSS',
'http_request' => 'Żądanie HTTP', 'http_request' => 'Żądanie HTTP',
@ -213,7 +212,6 @@
'delay' => 'Opóźnienie', 'delay' => 'Opóźnienie',
'condition' => 'Warunek', 'condition' => 'Warunek',
'publish' => 'Publikuj', 'publish' => 'Publikuj',
'webhook' => 'Webhook',
'end' => 'Koniec', 'end' => 'Koniec',
'end_summary' => 'Zatrzymuje automatyzację w tym miejscu', 'end_summary' => 'Zatrzymuje automatyzację w tym miejscu',
'fetch_rss' => 'Pobierz RSS', 'fetch_rss' => 'Pobierz RSS',
@ -326,11 +324,6 @@
'scheduled_offset' => 'Przesunięcie od wyzwolenia (minuty)', 'scheduled_offset' => 'Przesunięcie od wyzwolenia (minuty)',
'offset_summary' => ':mode · +:offset min', 'offset_summary' => ':mode · +:offset min',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'Metoda',
'payload_template' => 'Szablon ładunku (JSON)',
],
'end' => [ 'end' => [
'reason' => 'Powód (opcjonalnie)', 'reason' => 'Powód (opcjonalnie)',
'reason_placeholder' => 'np. Odfiltrowane przez warunek', 'reason_placeholder' => 'np. Odfiltrowane przez warunek',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'Graf automatyzacji zawiera cykl.', 'graph_contains_cycle' => 'Graf automatyzacji zawiera cykl.',
'only_failed_can_retry' => 'Tylko nieudane uruchomienia można ponowić.', 'only_failed_can_retry' => 'Tylko nieudane uruchomienia można ponowić.',
'no_generated_post' => 'Nie znaleziono wygenerowanego posta w uruchomieniu.', 'no_generated_post' => 'Nie znaleziono wygenerowanego posta w uruchomieniu.',
'webhook_server_error' => 'Błąd serwera webhooka.',
'webhook_request_failed' => 'Nie udało się zrealizować żądania webhooka.',
'webhook_missing_url' => 'W węźle webhooka brakuje adresu URL.',
'webhook_invalid_payload_json' => 'Szablon ładunku nie jest prawidłowym JSON-em.',
'url_not_allowed' => 'Adres URL żądania wskazuje na prywatny lub nieosiągalny adres i został zablokowany.', 'url_not_allowed' => 'Adres URL żądania wskazuje na prywatny lub nieosiągalny adres i został zablokowany.',
'node_no_longer_exists' => 'Węzeł :node_id już nie istnieje w automatyzacji.', 'node_no_longer_exists' => 'Węzeł :node_id już nie istnieje w automatyzacji.',
'no_trigger_connection' => 'Żaden węzeł nie jest połączony z węzłem wyzwalacza.', 'no_trigger_connection' => 'Żaden węzeł nie jest połączony z węzłem wyzwalacza.',

View file

@ -16,6 +16,7 @@
'signatures' => 'Sygnatury', 'signatures' => 'Sygnatury',
'labels' => 'Etykiety', 'labels' => 'Etykiety',
'assets' => 'Zasoby', 'assets' => 'Zasoby',
'webhooks' => 'Webhooki',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'Język: :name', 'language' => 'Język: :name',

139
lang/pl/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'Otrzymuj powiadomienia w czasie rzeczywistym, gdy posty są tworzone, planowane, odwoływane z planu, publikowane lub kończą się niepowodzeniem.',
'new' => 'Utwórz webhook',
'empty_title' => 'Nie ma jeszcze webhooków',
'empty_description' => 'Utwórz webhook, aby otrzymywać powiadomienia o zdarzeniach w czasie rzeczywistym.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'Nasłuchuje',
'status' => 'Status',
'last_sent' => 'Ostatnio wysłano',
],
'events_count' => '{1} :count zdarzenie|[2,*] :count zdarzeń',
'never' => 'Nigdy',
'status' => [
'enabled' => 'Aktywny',
'disabled' => 'Wyłączony',
'paused' => 'Wstrzymany',
],
'actions' => [
'view' => 'Zobacz szczegóły',
'copy_id' => 'Kopiuj ID webhooka',
'delete' => 'Usuń',
'edit' => 'Edytuj endpoint',
'enable' => 'Włącz endpoint',
'disable' => 'Wyłącz endpoint',
'rotate' => 'Obróć secret podpisu',
'send_test' => 'Wyślij zdarzenie testowe',
'replay' => 'Wyślij ponownie',
'reveal_secret' => 'Pokaż secret',
'hide_secret' => 'Ukryj secret',
'copy_secret' => 'Kopiuj secret',
],
'create' => [
'title' => 'Utwórz webhook',
'description' => 'Skonfiguruj endpoint, aby otrzymywać powiadomienia webhook.',
'endpoint' => 'URL endpointu',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'Zdarzenia',
'events_placeholder' => 'Wybierz zdarzenia...',
'events_selected' => '{1} :count zdarzenie wybrane|[2,*] :count zdarzeń wybranych',
'search_events' => 'Szukaj zdarzeń...',
'no_events' => 'Nie znaleziono zdarzeń',
'submit' => 'Utwórz webhook',
'cancel' => 'Anuluj',
],
'edit' => [
'title' => 'Edytuj endpoint',
'description' => 'Zaktualizuj URL endpointu i zdarzenia do nasłuchiwania.',
'submit' => 'Zapisz zmiany',
'cancel' => 'Anuluj',
],
'delete' => [
'title' => 'Usuń webhook',
'description' => 'Czy na pewno chcesz usunąć ten webhook? Nie będziesz już otrzymywać powiadomień o zdarzeniach na tym endpoincie.',
'confirm' => 'Usuń webhook',
'cancel' => 'Anuluj',
],
'rotate' => [
'title' => 'Obróć secret podpisu',
'description' => 'To wygeneruje nowy secret podpisu. Obecny secret przestanie działać od razu. Zaktualizuj swój endpoint, aby używał nowego secretu.',
'submit' => 'Obróć secret',
'cancel' => 'Anuluj',
],
'show' => [
'signing_secret' => 'Secret podpisu',
'last_sent' => 'Ostatnio wysłano :time',
'listening_for' => 'Nasłuchuje',
'http_status' => 'Status HTTP',
'status_code' => ':code - :reason',
'attempts' => 'Próby',
'delivered_at' => 'Dostarczono',
'response_body' => 'Treść odpowiedzi',
'response' => 'Odpowiedź',
'no_response_body' => 'Brak treści odpowiedzi',
'no_response' => 'Brak odpowiedzi',
'payload' => 'Payload wiadomości',
'empty_title' => 'Nie ma jeszcze zdarzeń',
'empty_description' => 'Gdy posty zostaną utworzone, zaplanowane, odwołane z planu lub opublikowane, zdarzenia webhooka pojawią się tutaj.',
],
'events' => [
'group_posts' => 'Posty',
'post_created' => 'Post utworzony',
'post_scheduled' => 'Post zaplanowany',
'post_unscheduled' => 'Planowanie anulowane',
'post_published' => 'Post opublikowany',
'post_partially_published' => 'Post częściowo opublikowany',
'post_failed' => 'Post nieudany',
'post_deleted' => 'Post usunięty',
],
'http_reasons' => [
'unknown' => 'Nieznany',
'200' => 'OK',
'201' => 'Utworzono',
'202' => 'Zaakceptowano',
'204' => 'Brak treści',
'400' => 'Nieprawidłowe żądanie',
'401' => 'Nieautoryzowany',
'403' => 'Zabronione',
'404' => 'Nie znaleziono',
'408' => 'Przekroczono czas',
'422' => 'Nieprzetwarzalna encja',
'429' => 'Zbyt wiele żądań',
'500' => 'Wewnętrzny błąd serwera',
'502' => 'Nieprawidłowa brama',
'503' => 'Usługa niedostępna',
'504' => 'Przekroczono czas bramy',
],
'copied' => [
'id' => 'ID webhooka skopiowane do schowka',
'secret' => 'Secret podpisu skopiowany do schowka',
'response' => 'Treść odpowiedzi skopiowana',
'payload' => 'Payload skopiowany',
],
'errors' => [
'endpoint_not_allowed' => 'Ten endpoint nie jest dozwolony.',
'endpoint_unreachable' => 'Endpoint jest nieosiągalny.',
'endpoint_http_status' => 'Endpoint zwrócił HTTP :status.',
],
'flash' => [
'created' => 'Webhook utworzony.',
'updated' => 'Webhook zaktualizowany.',
'deleted' => 'Webhook usunięty.',
'secret_rotated' => 'Secret podpisu obrócony.',
'replayed' => 'Zdarzenie webhooka wysłane ponownie.',
'tested' => 'Wysłano zdarzenie testowe.',
],
'mail' => [
'paused_subject' => 'Webhook wstrzymany: :endpoint',
'paused_title' => 'Webhook wstrzymany po powtarzających się błędach',
'paused_preview' => 'Wstrzymaliśmy webhook po 5 kolejnych błędach dostarczenia.',
'paused_body' => 'Wstrzymaliśmy webhook pod adresem :endpoint po 5 kolejnych błędach dostarczenia. Sprawdź endpoint i włącz go ponownie na stronie szczegółów webhooka.',
'paused_cta' => 'Zobacz webhook',
],
];

View file

@ -62,7 +62,6 @@
'delay' => 'Espera', 'delay' => 'Espera',
'condition' => 'Condição', 'condition' => 'Condição',
'publish' => 'Publicar', 'publish' => 'Publicar',
'webhook' => 'Webhook',
'end' => 'Fim', 'end' => 'Fim',
'fetch_rss' => 'Buscar RSS', 'fetch_rss' => 'Buscar RSS',
'http_request' => 'Requisição HTTP', 'http_request' => 'Requisição HTTP',
@ -213,7 +212,6 @@
'delay' => 'Esperar', 'delay' => 'Esperar',
'condition' => 'Condição', 'condition' => 'Condição',
'publish' => 'Publicar', 'publish' => 'Publicar',
'webhook' => 'Webhook',
'end' => 'Encerrar', 'end' => 'Encerrar',
'end_summary' => 'Encerra a automação aqui', 'end_summary' => 'Encerra a automação aqui',
'fetch_rss' => 'Buscar RSS', 'fetch_rss' => 'Buscar RSS',
@ -326,11 +324,6 @@
'scheduled_offset' => 'Atraso a partir do trigger (minutos)', 'scheduled_offset' => 'Atraso a partir do trigger (minutos)',
'offset_summary' => ':mode · +:offset min', 'offset_summary' => ':mode · +:offset min',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'Método',
'payload_template' => 'Template do payload (JSON)',
],
'end' => [ 'end' => [
'reason' => 'Motivo (opcional)', 'reason' => 'Motivo (opcional)',
'reason_placeholder' => 'ex: Filtrado pela condição', 'reason_placeholder' => 'ex: Filtrado pela condição',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'O grafo da automação contém um ciclo.', 'graph_contains_cycle' => 'O grafo da automação contém um ciclo.',
'only_failed_can_retry' => 'Apenas execuções que falharam podem ser repetidas.', 'only_failed_can_retry' => 'Apenas execuções que falharam podem ser repetidas.',
'no_generated_post' => 'Nenhum post gerado encontrado para esta execução.', 'no_generated_post' => 'Nenhum post gerado encontrado para esta execução.',
'webhook_server_error' => 'Erro no servidor do webhook.',
'webhook_request_failed' => 'Não foi possível completar a requisição do webhook.',
'webhook_missing_url' => 'O nó de webhook está sem a URL.',
'webhook_invalid_payload_json' => 'O template do payload não é um JSON válido.',
'url_not_allowed' => 'A URL da requisição aponta para um endereço privado ou inacessível e foi bloqueada.', 'url_not_allowed' => 'A URL da requisição aponta para um endereço privado ou inacessível e foi bloqueada.',
'node_no_longer_exists' => 'O nó :node_id não existe mais nesta automação.', 'node_no_longer_exists' => 'O nó :node_id não existe mais nesta automação.',
'no_trigger_connection' => 'Nenhum nó conectado ao nó de trigger.', 'no_trigger_connection' => 'Nenhum nó conectado ao nó de trigger.',

View file

@ -16,6 +16,7 @@
'signatures' => 'Assinaturas', 'signatures' => 'Assinaturas',
'labels' => 'Etiquetas', 'labels' => 'Etiquetas',
'assets' => 'Mídias', 'assets' => 'Mídias',
'webhooks' => 'Webhooks',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'Idioma: :name', 'language' => 'Idioma: :name',

139
lang/pt-BR/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'Receba notificações em tempo real quando posts forem criados, agendados, desagendados, publicados ou falharem.',
'new' => 'Criar webhook',
'empty_title' => 'Nenhum webhook ainda',
'empty_description' => 'Crie um webhook para receber notificações de eventos em tempo real.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'Escutando',
'status' => 'Status',
'last_sent' => 'Último envio',
],
'events_count' => '{1} :count evento|[2,*] :count eventos',
'never' => 'Nunca',
'status' => [
'enabled' => 'Ativo',
'disabled' => 'Desativado',
'paused' => 'Pausado',
],
'actions' => [
'view' => 'Ver detalhes',
'copy_id' => 'Copiar ID do webhook',
'delete' => 'Excluir',
'edit' => 'Editar endpoint',
'enable' => 'Ativar endpoint',
'disable' => 'Desativar endpoint',
'rotate' => 'Rotacionar secret de assinatura',
'send_test' => 'Enviar evento de teste',
'replay' => 'Reenviar',
'reveal_secret' => 'Mostrar secret',
'hide_secret' => 'Ocultar secret',
'copy_secret' => 'Copiar secret',
],
'create' => [
'title' => 'Criar webhook',
'description' => 'Configure um endpoint para receber notificações de webhook.',
'endpoint' => 'URL do endpoint',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'Eventos',
'events_placeholder' => 'Selecionar eventos...',
'events_selected' => '{1} :count evento selecionado|[2,*] :count eventos selecionados',
'search_events' => 'Buscar eventos...',
'no_events' => 'Nenhum evento encontrado',
'submit' => 'Criar webhook',
'cancel' => 'Cancelar',
],
'edit' => [
'title' => 'Editar endpoint',
'description' => 'Atualize a URL do endpoint e os eventos a escutar.',
'submit' => 'Salvar alterações',
'cancel' => 'Cancelar',
],
'delete' => [
'title' => 'Excluir webhook',
'description' => 'Tem certeza de que deseja excluir este webhook? Você não receberá mais notificações neste endpoint.',
'confirm' => 'Excluir webhook',
'cancel' => 'Cancelar',
],
'rotate' => [
'title' => 'Rotacionar secret de assinatura',
'description' => 'Isso gera um novo secret de assinatura. O secret atual para de funcionar imediatamente. Atualize seu endpoint para usar o novo secret.',
'submit' => 'Rotacionar secret',
'cancel' => 'Cancelar',
],
'show' => [
'signing_secret' => 'Secret de assinatura',
'last_sent' => 'Último envio :time',
'listening_for' => 'Escutando',
'http_status' => 'Status HTTP',
'status_code' => ':code - :reason',
'attempts' => 'Tentativas',
'delivered_at' => 'Entregue em',
'response_body' => 'Corpo da resposta',
'response' => 'Resposta',
'no_response_body' => 'Sem corpo de resposta',
'no_response' => 'Sem resposta',
'payload' => 'Payload da mensagem',
'empty_title' => 'Nenhum evento ainda',
'empty_description' => 'Quando posts forem criados, agendados, desagendados ou publicados, os eventos do webhook aparecem aqui.',
],
'events' => [
'group_posts' => 'Posts',
'post_created' => 'Post criado',
'post_scheduled' => 'Post agendado',
'post_unscheduled' => 'Post desagendado',
'post_published' => 'Post publicado',
'post_partially_published' => 'Post parcialmente publicado',
'post_failed' => 'Post falhou',
'post_deleted' => 'Post excluído',
],
'http_reasons' => [
'unknown' => 'Desconhecido',
'200' => 'OK',
'201' => 'Criado',
'202' => 'Aceito',
'204' => 'Sem conteúdo',
'400' => 'Requisição inválida',
'401' => 'Não autorizado',
'403' => 'Proibido',
'404' => 'Não encontrado',
'408' => 'Tempo esgotado',
'422' => 'Entidade não processável',
'429' => 'Muitas requisições',
'500' => 'Erro interno do servidor',
'502' => 'Gateway inválido',
'503' => 'Serviço indisponível',
'504' => 'Tempo esgotado do gateway',
],
'copied' => [
'id' => 'ID do webhook copiado',
'secret' => 'Secret de assinatura copiado',
'response' => 'Corpo da resposta copiado',
'payload' => 'Payload copiado',
],
'errors' => [
'endpoint_not_allowed' => 'Este endpoint não é permitido.',
'endpoint_unreachable' => 'O endpoint não está acessível.',
'endpoint_http_status' => 'O endpoint retornou HTTP :status.',
],
'flash' => [
'created' => 'Webhook criado.',
'updated' => 'Webhook atualizado.',
'deleted' => 'Webhook excluído.',
'secret_rotated' => 'Secret de assinatura rotacionado.',
'replayed' => 'Evento do webhook reenviado.',
'tested' => 'Evento de teste enviado.',
],
'mail' => [
'paused_subject' => 'Webhook pausado: :endpoint',
'paused_title' => 'Webhook pausado após falhas repetidas',
'paused_preview' => 'Pausamos um webhook após 5 falhas consecutivas de entrega.',
'paused_body' => 'Pausamos o webhook em :endpoint após 5 falhas consecutivas de entrega. Revise o endpoint e reative-o na página de detalhes do webhook.',
'paused_cta' => 'Ver webhook',
],
];

View file

@ -62,7 +62,6 @@
'delay' => 'Задержка', 'delay' => 'Задержка',
'condition' => 'Условие', 'condition' => 'Условие',
'publish' => 'Публикация', 'publish' => 'Публикация',
'webhook' => 'Webhook',
'end' => 'Конец', 'end' => 'Конец',
'fetch_rss' => 'Получить RSS', 'fetch_rss' => 'Получить RSS',
'http_request' => 'HTTP-запрос', 'http_request' => 'HTTP-запрос',
@ -213,7 +212,6 @@
'delay' => 'Задержка', 'delay' => 'Задержка',
'condition' => 'Условие', 'condition' => 'Условие',
'publish' => 'Публикация', 'publish' => 'Публикация',
'webhook' => 'Webhook',
'end' => 'Конец', 'end' => 'Конец',
'end_summary' => 'Останавливает автоматизацию здесь', 'end_summary' => 'Останавливает автоматизацию здесь',
'fetch_rss' => 'Получить RSS', 'fetch_rss' => 'Получить RSS',
@ -326,11 +324,6 @@
'scheduled_offset' => 'Смещение от триггера (минуты)', 'scheduled_offset' => 'Смещение от триггера (минуты)',
'offset_summary' => ':mode · +:offset мин', 'offset_summary' => ':mode · +:offset мин',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'Метод',
'payload_template' => 'Шаблон полезной нагрузки (JSON)',
],
'end' => [ 'end' => [
'reason' => 'Причина (необязательно)', 'reason' => 'Причина (необязательно)',
'reason_placeholder' => 'например, Отфильтровано условием', 'reason_placeholder' => 'например, Отфильтровано условием',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'Граф автоматизации содержит цикл.', 'graph_contains_cycle' => 'Граф автоматизации содержит цикл.',
'only_failed_can_retry' => 'Повторить можно только запуски, завершившиеся с ошибкой.', 'only_failed_can_retry' => 'Повторить можно только запуски, завершившиеся с ошибкой.',
'no_generated_post' => 'В запуске не найдено сгенерированного поста.', 'no_generated_post' => 'В запуске не найдено сгенерированного поста.',
'webhook_server_error' => 'Ошибка сервера webhook.',
'webhook_request_failed' => 'Не удалось выполнить запрос webhook.',
'webhook_missing_url' => 'В узле webhook отсутствует URL.',
'webhook_invalid_payload_json' => 'Шаблон полезной нагрузки не является корректным JSON.',
'url_not_allowed' => 'URL запроса указывает на приватный или недоступный адрес и был заблокирован.', 'url_not_allowed' => 'URL запроса указывает на приватный или недоступный адрес и был заблокирован.',
'node_no_longer_exists' => 'Узел :node_id больше не существует в автоматизации.', 'node_no_longer_exists' => 'Узел :node_id больше не существует в автоматизации.',
'no_trigger_connection' => 'К узлу-триггеру не подключён ни один узел.', 'no_trigger_connection' => 'К узлу-триггеру не подключён ни один узел.',

View file

@ -16,6 +16,7 @@
'signatures' => 'Подписи', 'signatures' => 'Подписи',
'labels' => 'Метки', 'labels' => 'Метки',
'assets' => 'Медиафайлы', 'assets' => 'Медиафайлы',
'webhooks' => 'Вебхуки',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'Язык: :name', 'language' => 'Язык: :name',

139
lang/ru/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'Получайте уведомления в реальном времени, когда посты создаются, планируются, снимаются с плана, публикуются или завершаются ошибкой.',
'new' => 'Создать вебхук',
'empty_title' => 'Вебхуков пока нет',
'empty_description' => 'Создайте вебхук, чтобы получать уведомления о событиях в реальном времени.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'Слушает',
'status' => 'Статус',
'last_sent' => 'Последняя отправка',
],
'events_count' => '{1} :count событие|[2,*] :count событий',
'never' => 'Никогда',
'status' => [
'enabled' => 'Включён',
'disabled' => 'Отключён',
'paused' => 'Приостановлен',
],
'actions' => [
'view' => 'Подробности',
'copy_id' => 'Скопировать ID вебхука',
'delete' => 'Удалить',
'edit' => 'Изменить endpoint',
'enable' => 'Включить endpoint',
'disable' => 'Отключить endpoint',
'rotate' => 'Сменить секрет подписи',
'send_test' => 'Отправить тестовое событие',
'replay' => 'Отправить снова',
'reveal_secret' => 'Показать секрет',
'hide_secret' => 'Скрыть секрет',
'copy_secret' => 'Скопировать секрет',
],
'create' => [
'title' => 'Создать вебхук',
'description' => 'Настройте endpoint для получения уведомлений вебхука.',
'endpoint' => 'URL endpoint',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'События',
'events_placeholder' => 'Выберите события...',
'events_selected' => '{1} :count событие выбрано|[2,*] :count событий выбрано',
'search_events' => 'Поиск событий...',
'no_events' => 'События не найдены',
'submit' => 'Создать вебхук',
'cancel' => 'Отмена',
],
'edit' => [
'title' => 'Изменить endpoint',
'description' => 'Обновите URL endpoint и события для прослушивания.',
'submit' => 'Сохранить изменения',
'cancel' => 'Отмена',
],
'delete' => [
'title' => 'Удалить вебхук',
'description' => 'Удалить этот вебхук? Вы больше не будете получать уведомления о событиях на этом endpoint.',
'confirm' => 'Удалить вебхук',
'cancel' => 'Отмена',
],
'rotate' => [
'title' => 'Сменить секрет подписи',
'description' => 'Будет создан новый секрет подписи. Текущий секрет сразу перестанет работать. Обновите endpoint, чтобы использовать новый секрет.',
'submit' => 'Сменить секрет',
'cancel' => 'Отмена',
],
'show' => [
'signing_secret' => 'Секрет подписи',
'last_sent' => 'Последняя отправка :time',
'listening_for' => 'Слушает',
'http_status' => 'Статус HTTP',
'status_code' => ':code - :reason',
'attempts' => 'Попытки',
'delivered_at' => 'Доставлено',
'response_body' => 'Тело ответа',
'response' => 'Ответ',
'no_response_body' => 'Нет тела ответа',
'no_response' => 'Нет ответа',
'payload' => 'Payload сообщения',
'empty_title' => 'Событий пока нет',
'empty_description' => 'Когда посты создаются, планируются, снимаются с плана или публикуются, события вебхука появятся здесь.',
],
'events' => [
'group_posts' => 'Посты',
'post_created' => 'Пост создан',
'post_scheduled' => 'Пост запланирован',
'post_unscheduled' => 'Планирование отменено',
'post_published' => 'Пост опубликован',
'post_partially_published' => 'Пост частично опубликован',
'post_failed' => 'Ошибка публикации',
'post_deleted' => 'Пост удалён',
],
'http_reasons' => [
'unknown' => 'Неизвестно',
'200' => 'OK',
'201' => 'Создано',
'202' => 'Принято',
'204' => 'Нет содержимого',
'400' => 'Неверный запрос',
'401' => 'Не авторизован',
'403' => 'Запрещено',
'404' => 'Не найдено',
'408' => 'Истекло время ожидания',
'422' => 'Необрабатываемая сущность',
'429' => 'Слишком много запросов',
'500' => 'Внутренняя ошибка сервера',
'502' => 'Ошибочный шлюз',
'503' => 'Сервис недоступен',
'504' => 'Истекло время шлюза',
],
'copied' => [
'id' => 'ID вебхука скопирован в буфер обмена',
'secret' => 'Секрет подписи скопирован в буфер обмена',
'response' => 'Тело ответа скопировано',
'payload' => 'Payload скопирован',
],
'errors' => [
'endpoint_not_allowed' => 'Этот endpoint не разрешён.',
'endpoint_unreachable' => 'Endpoint недоступен.',
'endpoint_http_status' => 'Endpoint вернул HTTP :status.',
],
'flash' => [
'created' => 'Вебхук создан.',
'updated' => 'Вебхук обновлён.',
'deleted' => 'Вебхук удалён.',
'secret_rotated' => 'Секрет подписи изменён.',
'replayed' => 'Событие вебхука отправлено снова.',
'tested' => 'Тестовое событие отправлено.',
],
'mail' => [
'paused_subject' => 'Вебхук приостановлен: :endpoint',
'paused_title' => 'Вебхук приостановлен после повторных ошибок',
'paused_preview' => 'Мы приостановили вебхук после 5 подряд неудачных доставок.',
'paused_body' => 'Мы приостановили вебхук на :endpoint после 5 подряд неудачных доставок. Проверьте endpoint и включите его снова на странице вебхука.',
'paused_cta' => 'Открыть вебхук',
],
];

View file

@ -62,7 +62,6 @@
'delay' => 'Gecikme', 'delay' => 'Gecikme',
'condition' => 'Koşul', 'condition' => 'Koşul',
'publish' => 'Yayınla', 'publish' => 'Yayınla',
'webhook' => 'Webhook',
'end' => 'Bitir', 'end' => 'Bitir',
'fetch_rss' => 'RSS Getir', 'fetch_rss' => 'RSS Getir',
'http_request' => 'HTTP isteği', 'http_request' => 'HTTP isteği',
@ -213,7 +212,6 @@
'delay' => 'Gecikme', 'delay' => 'Gecikme',
'condition' => 'Koşul', 'condition' => 'Koşul',
'publish' => 'Yayınla', 'publish' => 'Yayınla',
'webhook' => 'Webhook',
'end' => 'Bitir', 'end' => 'Bitir',
'end_summary' => 'Otomasyonu burada durdurur', 'end_summary' => 'Otomasyonu burada durdurur',
'fetch_rss' => 'RSS Getir', 'fetch_rss' => 'RSS Getir',
@ -326,11 +324,6 @@
'scheduled_offset' => 'Tetikleyiciden kayma (dakika)', 'scheduled_offset' => 'Tetikleyiciden kayma (dakika)',
'offset_summary' => ':mode · +:offset dk', 'offset_summary' => ':mode · +:offset dk',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'Yöntem',
'payload_template' => 'Yük şablonu (JSON)',
],
'end' => [ 'end' => [
'reason' => 'Neden (isteğe bağlı)', 'reason' => 'Neden (isteğe bağlı)',
'reason_placeholder' => 'örn. Koşul tarafından filtrelendi', 'reason_placeholder' => 'örn. Koşul tarafından filtrelendi',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'Otomasyon grafiği bir döngü içeriyor.', 'graph_contains_cycle' => 'Otomasyon grafiği bir döngü içeriyor.',
'only_failed_can_retry' => 'Yalnızca başarısız çalıştırmalar yeniden denenebilir.', 'only_failed_can_retry' => 'Yalnızca başarısız çalıştırmalar yeniden denenebilir.',
'no_generated_post' => 'Çalıştırmada oluşturulmuş gönderi bulunamadı.', 'no_generated_post' => 'Çalıştırmada oluşturulmuş gönderi bulunamadı.',
'webhook_server_error' => 'Webhook sunucu hatası.',
'webhook_request_failed' => 'Webhook isteği tamamlanamadı.',
'webhook_missing_url' => 'Webhook düğümünde URL eksik.',
'webhook_invalid_payload_json' => 'Yük şablonu geçerli JSON değil.',
'url_not_allowed' => 'İstek URL\'si özel veya erişilemez bir adrese işaret ediyor ve engellendi.', 'url_not_allowed' => 'İstek URL\'si özel veya erişilemez bir adrese işaret ediyor ve engellendi.',
'node_no_longer_exists' => ':node_id düğümü artık otomasyonda yok.', 'node_no_longer_exists' => ':node_id düğümü artık otomasyonda yok.',
'no_trigger_connection' => 'Tetikleyici düğümüne bağlı düğüm yok.', 'no_trigger_connection' => 'Tetikleyici düğümüne bağlı düğüm yok.',

View file

@ -16,6 +16,7 @@
'signatures' => 'İmzalar', 'signatures' => 'İmzalar',
'labels' => 'Etiketler', 'labels' => 'Etiketler',
'assets' => 'Varlıklar', 'assets' => 'Varlıklar',
'webhooks' => 'Webhooklar',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'Dil: :name', 'language' => 'Dil: :name',

139
lang/tr/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'Gönderiler oluşturulduğunda, zamanlandığında, zamanlaması kaldırıldığında, yayınlandığında veya başarısız olduğunda anlık bildirim alın.',
'new' => 'Webhook oluştur',
'empty_title' => 'Henüz webhook yok',
'empty_description' => 'Anlık olay bildirimleri almak için bir webhook oluşturun.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'Dinlenenler',
'status' => 'Durum',
'last_sent' => 'Son gönderim',
],
'events_count' => '{1} :count olay|[2,*] :count olay',
'never' => 'Hiçbir zaman',
'status' => [
'enabled' => 'Etkin',
'disabled' => 'Devre dışı',
'paused' => 'Duraklatıldı',
],
'actions' => [
'view' => 'Ayrıntıları gör',
'copy_id' => 'Webhook kimliğini kopyala',
'delete' => 'Sil',
'edit' => 'Endpointi düzenle',
'enable' => 'Endpointi etkinleştir',
'disable' => 'Endpointi devre dışı bırak',
'rotate' => 'İmza secretini yenile',
'send_test' => 'Test olayı gönder',
'replay' => 'Yeniden gönder',
'reveal_secret' => 'Secreti göster',
'hide_secret' => 'Secreti gizle',
'copy_secret' => 'Secreti kopyala',
],
'create' => [
'title' => 'Webhook oluştur',
'description' => 'Webhook bildirimleri almak için bir endpoint yapılandırın.',
'endpoint' => 'Endpoint URL\'si',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'Olaylar',
'events_placeholder' => 'Olay seçin...',
'events_selected' => '{1} :count olay seçildi|[2,*] :count olay seçildi',
'search_events' => 'Olay ara...',
'no_events' => 'Olay bulunamadı',
'submit' => 'Webhook oluştur',
'cancel' => 'İptal',
],
'edit' => [
'title' => 'Endpointi düzenle',
'description' => 'Endpoint URL\'sini ve dinlenecek olayları güncelleyin.',
'submit' => 'Değişiklikleri kaydet',
'cancel' => 'İptal',
],
'delete' => [
'title' => 'Webhooku sil',
'description' => 'Bu webhooku silmek istediğinize emin misiniz? Bu endpointte artık olay bildirimleri almazsınız.',
'confirm' => 'Webhooku sil',
'cancel' => 'İptal',
],
'rotate' => [
'title' => 'İmza secretini yenile',
'description' => 'Bu işlem yeni bir imza secreti üretir. Mevcut secret hemen çalışmayı durdurur. Endpointinizi yeni secreti kullanacak şekilde güncelleyin.',
'submit' => 'Secreti yenile',
'cancel' => 'İptal',
],
'show' => [
'signing_secret' => 'İmza secreti',
'last_sent' => 'Son gönderim :time',
'listening_for' => 'Dinlenenler',
'http_status' => 'HTTP durumu',
'status_code' => ':code - :reason',
'attempts' => 'Denemeler',
'delivered_at' => 'Teslim edildi',
'response_body' => 'Yanıt gövdesi',
'response' => 'Yanıt',
'no_response_body' => 'Yanıt gövdesi yok',
'no_response' => 'Yanıt yok',
'payload' => 'Mesaj payloadı',
'empty_title' => 'Henüz webhook olayı yok',
'empty_description' => 'Gönderiler oluşturulduğunda, zamanlandığında, zamanlaması kaldırıldığında veya yayınlandığında webhook olaylarını burada görürsünüz.',
],
'events' => [
'group_posts' => 'Gönderiler',
'post_created' => 'Gönderi oluşturuldu',
'post_scheduled' => 'Gönderi zamanlandı',
'post_unscheduled' => 'Zamanlama kaldırıldı',
'post_published' => 'Gönderi yayınlandı',
'post_partially_published' => 'Gönderi kısmen yayınlandı',
'post_failed' => 'Gönderi başarısız',
'post_deleted' => 'Gönderi silindi',
],
'http_reasons' => [
'unknown' => 'Bilinmiyor',
'200' => 'OK',
'201' => 'Oluşturuldu',
'202' => 'Kabul edildi',
'204' => 'İçerik yok',
'400' => 'Hatalı istek',
'401' => 'Yetkisiz',
'403' => 'Yasak',
'404' => 'Bulunamadı',
'408' => 'İstek zaman aşımı',
'422' => 'İşlenemeyen varlık',
'429' => 'Çok fazla istek',
'500' => 'Sunucu iç hatası',
'502' => 'Geçersiz ağ geçidi',
'503' => 'Hizmet kullanılamıyor',
'504' => 'Ağ geçidi zaman aşımı',
],
'copied' => [
'id' => 'Webhook kimliği panoya kopyalandı',
'secret' => 'İmza secreti panoya kopyalandı',
'response' => 'Yanıt gövdesi kopyalandı',
'payload' => 'Payload kopyalandı',
],
'errors' => [
'endpoint_not_allowed' => 'Bu endpointe izin verilmiyor.',
'endpoint_unreachable' => 'Endpoint erişilebilir değil.',
'endpoint_http_status' => 'Endpoint HTTP :status döndürdü.',
],
'flash' => [
'created' => 'Webhook oluşturuldu.',
'updated' => 'Webhook güncellendi.',
'deleted' => 'Webhook silindi.',
'secret_rotated' => 'İmza secreti yenilendi.',
'replayed' => 'Webhook olayı yeniden gönderildi.',
'tested' => 'Test olayı gönderildi.',
],
'mail' => [
'paused_subject' => 'Webhook duraklatıldı: :endpoint',
'paused_title' => 'Webhook tekrarlanan hatalardan sonra duraklatıldı',
'paused_preview' => 'Üst üste 5 teslim hatasından sonra bir webhooku duraklattık.',
'paused_body' => ':endpoint adresindeki webhooku üst üste 5 teslim hatasından sonra duraklattık. Endpointi gözden geçirin ve webhook ayrıntı sayfasından yeniden etkinleştirin.',
'paused_cta' => 'Webhooku gör',
],
];

View file

@ -62,7 +62,6 @@
'delay' => 'Затримка', 'delay' => 'Затримка',
'condition' => 'Умова', 'condition' => 'Умова',
'publish' => 'Публікація', 'publish' => 'Публікація',
'webhook' => 'Webhook',
'end' => 'Кінець', 'end' => 'Кінець',
'fetch_rss' => 'Отримати RSS', 'fetch_rss' => 'Отримати RSS',
'http_request' => 'HTTP-запит', 'http_request' => 'HTTP-запит',
@ -213,7 +212,6 @@
'delay' => 'Затримка', 'delay' => 'Затримка',
'condition' => 'Умова', 'condition' => 'Умова',
'publish' => 'Публікація', 'publish' => 'Публікація',
'webhook' => 'Webhook',
'end' => 'Кінець', 'end' => 'Кінець',
'end_summary' => 'Зупиняє автоматизацію тут', 'end_summary' => 'Зупиняє автоматизацію тут',
'fetch_rss' => 'Отримати RSS', 'fetch_rss' => 'Отримати RSS',
@ -326,11 +324,6 @@
'scheduled_offset' => 'Зміщення від тригера (хвилини)', 'scheduled_offset' => 'Зміщення від тригера (хвилини)',
'offset_summary' => ':mode · +:offset хв', 'offset_summary' => ':mode · +:offset хв',
], ],
'webhook' => [
'url' => 'URL',
'method' => 'Метод',
'payload_template' => 'Шаблон payload (JSON)',
],
'end' => [ 'end' => [
'reason' => 'Причина (необов’язково)', 'reason' => 'Причина (необов’язково)',
'reason_placeholder' => 'напр. Відфільтровано умовою', 'reason_placeholder' => 'напр. Відфільтровано умовою',
@ -394,10 +387,6 @@
'graph_contains_cycle' => 'Граф автоматизації містить цикл.', 'graph_contains_cycle' => 'Граф автоматизації містить цикл.',
'only_failed_can_retry' => 'Повторити можна лише запуски з помилкою.', 'only_failed_can_retry' => 'Повторити можна лише запуски з помилкою.',
'no_generated_post' => 'У запуску не знайдено згенерованого поста.', 'no_generated_post' => 'У запуску не знайдено згенерованого поста.',
'webhook_server_error' => 'Помилка сервера webhook.',
'webhook_request_failed' => 'Не вдалося виконати запит webhook.',
'webhook_missing_url' => 'У вузлі webhook відсутній URL.',
'webhook_invalid_payload_json' => 'Шаблон payload не є коректним JSON.',
'url_not_allowed' => 'URL запиту вказує на приватну або недоступну адресу і було заблоковано.', 'url_not_allowed' => 'URL запиту вказує на приватну або недоступну адресу і було заблоковано.',
'node_no_longer_exists' => 'Вузол :node_id більше не існує в автоматизації.', 'node_no_longer_exists' => 'Вузол :node_id більше не існує в автоматизації.',
'no_trigger_connection' => 'До вузла-тригера не підключено жодного вузла.', 'no_trigger_connection' => 'До вузла-тригера не підключено жодного вузла.',

View file

@ -16,6 +16,7 @@
'signatures' => 'Підписи', 'signatures' => 'Підписи',
'labels' => 'Мітки', 'labels' => 'Мітки',
'assets' => 'Медіафайли', 'assets' => 'Медіафайли',
'webhooks' => 'Вебхуки',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => 'Мова: :name', 'language' => 'Мова: :name',

139
lang/uk/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => 'Отримуйте сповіщення в реальному часі, коли пости створюються, плануються, знімаються з плану, публікуються або завершуються помилкою.',
'new' => 'Створити вебхук',
'empty_title' => 'Вебхуків ще немає',
'empty_description' => 'Створіть вебхук, щоб отримувати сповіщення про події в реальному часі.',
'table' => [
'endpoint' => 'Endpoint',
'events' => 'Слухає',
'status' => 'Статус',
'last_sent' => 'Остання відправка',
],
'events_count' => '{1} :count подія|[2,*] :count подій',
'never' => 'Ніколи',
'status' => [
'enabled' => 'Увімкнено',
'disabled' => 'Вимкнено',
'paused' => 'Призупинено',
],
'actions' => [
'view' => 'Переглянути деталі',
'copy_id' => 'Скопіювати ID вебхука',
'delete' => 'Видалити',
'edit' => 'Редагувати endpoint',
'enable' => 'Увімкнути endpoint',
'disable' => 'Вимкнути endpoint',
'rotate' => 'Змінити секрет підпису',
'send_test' => 'Надіслати тестову подію',
'replay' => 'Надіслати знову',
'reveal_secret' => 'Показати секрет',
'hide_secret' => 'Приховати секрет',
'copy_secret' => 'Скопіювати секрет',
],
'create' => [
'title' => 'Створити вебхук',
'description' => 'Налаштуйте endpoint, щоб отримувати сповіщення вебхука.',
'endpoint' => 'URL endpoint',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => 'Події',
'events_placeholder' => 'Виберіть події...',
'events_selected' => '{1} :count подію вибрано|[2,*] :count подій вибрано',
'search_events' => 'Шукати події...',
'no_events' => 'Подій не знайдено',
'submit' => 'Створити вебхук',
'cancel' => 'Скасувати',
],
'edit' => [
'title' => 'Редагувати endpoint',
'description' => 'Оновіть URL endpoint і події для прослуховування.',
'submit' => 'Зберегти зміни',
'cancel' => 'Скасувати',
],
'delete' => [
'title' => 'Видалити вебхук',
'description' => 'Видалити цей вебхук? Ви більше не отримуватимете сповіщення про події на цьому endpoint.',
'confirm' => 'Видалити вебхук',
'cancel' => 'Скасувати',
],
'rotate' => [
'title' => 'Змінити секрет підпису',
'description' => 'Буде створено новий секрет підпису. Поточний секрет одразу перестане працювати. Оновіть endpoint, щоб використовувати новий секрет.',
'submit' => 'Змінити секрет',
'cancel' => 'Скасувати',
],
'show' => [
'signing_secret' => 'Секрет підпису',
'last_sent' => 'Остання відправка :time',
'listening_for' => 'Слухає',
'http_status' => 'Статус HTTP',
'status_code' => ':code - :reason',
'attempts' => 'Спроби',
'delivered_at' => 'Доставлено',
'response_body' => 'Тіло відповіді',
'response' => 'Відповідь',
'no_response_body' => 'Немає тіла відповіді',
'no_response' => 'Немає відповіді',
'payload' => 'Payload повідомлення',
'empty_title' => 'Подій ще немає',
'empty_description' => 'Коли пости створюються, плануються, знімаються з плану або публікуються, події вебхука з\'являться тут.',
],
'events' => [
'group_posts' => 'Пости',
'post_created' => 'Пост створено',
'post_scheduled' => 'Пост заплановано',
'post_unscheduled' => 'Планування скасовано',
'post_published' => 'Пост опубліковано',
'post_partially_published' => 'Пост частково опубліковано',
'post_failed' => 'Помилка публікації',
'post_deleted' => 'Пост видалено',
],
'http_reasons' => [
'unknown' => 'Невідомо',
'200' => 'OK',
'201' => 'Створено',
'202' => 'Прийнято',
'204' => 'Немає вмісту',
'400' => 'Неправильний запит',
'401' => 'Не авторизовано',
'403' => 'Заборонено',
'404' => 'Не знайдено',
'408' => 'Час очікування минув',
'422' => 'Необроблювана сутність',
'429' => 'Забагато запитів',
'500' => 'Внутрішня помилка сервера',
'502' => 'Помилковий шлюз',
'503' => 'Сервіс недоступний',
'504' => 'Час очікування шлюзу минув',
],
'copied' => [
'id' => 'ID вебхука скопійовано в буфер обміну',
'secret' => 'Секрет підпису скопійовано в буфер обміну',
'response' => 'Тіло відповіді скопійовано',
'payload' => 'Payload скопійовано',
],
'errors' => [
'endpoint_not_allowed' => 'Цей endpoint не дозволений.',
'endpoint_unreachable' => 'Endpoint недоступний.',
'endpoint_http_status' => 'Endpoint повернув HTTP :status.',
],
'flash' => [
'created' => 'Вебхук створено.',
'updated' => 'Вебхук оновлено.',
'deleted' => 'Вебхук видалено.',
'secret_rotated' => 'Секрет підпису змінено.',
'replayed' => 'Подію вебхука надіслано знову.',
'tested' => 'Тестову подію надіслано.',
],
'mail' => [
'paused_subject' => 'Вебхук призупинено: :endpoint',
'paused_title' => 'Вебхук призупинено після повторних помилок',
'paused_preview' => 'Ми призупинили вебхук після 5 підряд невдалих доставок.',
'paused_body' => 'Ми призупинили вебхук на :endpoint після 5 підряд невдалих доставок. Перевірте endpoint і ввімкніть його знову на сторінці вебхука.',
'paused_cta' => 'Відкрити вебхук',
],
];

View file

@ -62,7 +62,6 @@
'delay' => '延迟', 'delay' => '延迟',
'condition' => '条件', 'condition' => '条件',
'publish' => '发布', 'publish' => '发布',
'webhook' => 'Webhook',
'end' => '结束', 'end' => '结束',
'fetch_rss' => '抓取 RSS', 'fetch_rss' => '抓取 RSS',
'http_request' => 'HTTP 请求', 'http_request' => 'HTTP 请求',
@ -213,7 +212,6 @@
'delay' => '延迟', 'delay' => '延迟',
'condition' => '条件', 'condition' => '条件',
'publish' => '发布', 'publish' => '发布',
'webhook' => 'Webhook',
'end' => '结束', 'end' => '结束',
'end_summary' => '在此停止自动化', 'end_summary' => '在此停止自动化',
'fetch_rss' => '抓取 RSS', 'fetch_rss' => '抓取 RSS',
@ -326,11 +324,6 @@
'scheduled_offset' => '相对触发的延迟(分钟)', 'scheduled_offset' => '相对触发的延迟(分钟)',
'offset_summary' => ':mode · +:offset 分钟', 'offset_summary' => ':mode · +:offset 分钟',
], ],
'webhook' => [
'url' => 'URL',
'method' => '方法',
'payload_template' => '载荷模板JSON',
],
'end' => [ 'end' => [
'reason' => '原因(可选)', 'reason' => '原因(可选)',
'reason_placeholder' => '例如 被条件过滤掉', 'reason_placeholder' => '例如 被条件过滤掉',
@ -394,10 +387,6 @@
'graph_contains_cycle' => '自动化流程图中包含环路。', 'graph_contains_cycle' => '自动化流程图中包含环路。',
'only_failed_can_retry' => '只有失败的运行才能重试。', 'only_failed_can_retry' => '只有失败的运行才能重试。',
'no_generated_post' => '在该运行中未找到已生成的帖子。', 'no_generated_post' => '在该运行中未找到已生成的帖子。',
'webhook_server_error' => 'Webhook 服务器错误。',
'webhook_request_failed' => 'Webhook 请求无法完成。',
'webhook_missing_url' => 'Webhook 节点缺少 URL。',
'webhook_invalid_payload_json' => '载荷模板不是有效的 JSON。',
'url_not_allowed' => '请求 URL 指向私有或无法访问的地址,已被拦截。', 'url_not_allowed' => '请求 URL 指向私有或无法访问的地址,已被拦截。',
'node_no_longer_exists' => '节点 :node_id 已不存在于该自动化中。', 'node_no_longer_exists' => '节点 :node_id 已不存在于该自动化中。',
'no_trigger_connection' => '没有节点连接到触发器节点。', 'no_trigger_connection' => '没有节点连接到触发器节点。',

View file

@ -16,6 +16,7 @@
'signatures' => '签名', 'signatures' => '签名',
'labels' => '标签', 'labels' => '标签',
'assets' => '素材库', 'assets' => '素材库',
'webhooks' => 'Webhooks',
'mcp' => 'MCP', 'mcp' => 'MCP',
], ],
'language' => '语言::name', 'language' => '语言::name',

139
lang/zh/webhooks.php Normal file
View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
return [
'title' => 'Webhooks',
'description' => '在帖子被创建、排期、取消排期、发布或失败时接收实时通知。',
'new' => '创建 webhook',
'empty_title' => '还没有 webhook',
'empty_description' => '创建一个 webhook即可实时接收事件通知。',
'table' => [
'endpoint' => 'Endpoint',
'events' => '正在监听',
'status' => '状态',
'last_sent' => '上次发送',
],
'events_count' => '{1} :count 个事件|[2,*] :count 个事件',
'never' => '从未',
'status' => [
'enabled' => '已启用',
'disabled' => '已停用',
'paused' => '已暂停',
],
'actions' => [
'view' => '查看详情',
'copy_id' => '复制 webhook ID',
'delete' => '删除',
'edit' => '编辑 endpoint',
'enable' => '启用 endpoint',
'disable' => '停用 endpoint',
'rotate' => '轮换签名密钥',
'send_test' => '发送测试事件',
'replay' => '重新发送',
'reveal_secret' => '显示密钥',
'hide_secret' => '隐藏密钥',
'copy_secret' => '复制密钥',
],
'create' => [
'title' => '创建 webhook',
'description' => '配置一个 endpoint 以接收 webhook 通知。',
'endpoint' => 'Endpoint URL',
'endpoint_placeholder' => 'https://example.com/webhooks',
'events' => '事件',
'events_placeholder' => '选择事件...',
'events_selected' => '{1} 已选择 :count 个事件|[2,*] 已选择 :count 个事件',
'search_events' => '搜索事件...',
'no_events' => '未找到事件',
'submit' => '创建 webhook',
'cancel' => '取消',
],
'edit' => [
'title' => '编辑 endpoint',
'description' => '更新 endpoint URL 以及要监听的事件。',
'submit' => '保存更改',
'cancel' => '取消',
],
'delete' => [
'title' => '删除 webhook',
'description' => '确定要删除此 webhook 吗?此后你将不再在此 endpoint 收到事件通知。',
'confirm' => '删除 webhook',
'cancel' => '取消',
],
'rotate' => [
'title' => '轮换签名密钥',
'description' => '这将生成新的签名密钥。当前密钥会立即失效。请更新你的 endpoint 以使用新密钥。',
'submit' => '轮换密钥',
'cancel' => '取消',
],
'show' => [
'signing_secret' => '签名密钥',
'last_sent' => '上次发送 :time',
'listening_for' => '正在监听',
'http_status' => 'HTTP 状态',
'status_code' => ':code - :reason',
'attempts' => '尝试次数',
'delivered_at' => '送达时间',
'response_body' => '响应正文',
'response' => '响应',
'no_response_body' => '无响应正文',
'no_response' => '无响应',
'payload' => '消息 payload',
'empty_title' => '还没有 webhook 事件',
'empty_description' => '帖子被创建、排期、取消排期或发布后webhook 事件会显示在这里。',
],
'events' => [
'group_posts' => '帖子',
'post_created' => '帖子已创建',
'post_scheduled' => '帖子已排期',
'post_unscheduled' => '已取消排期',
'post_published' => '帖子已发布',
'post_partially_published' => '帖子部分发布',
'post_failed' => '帖子失败',
'post_deleted' => '帖子已删除',
],
'http_reasons' => [
'unknown' => '未知',
'200' => 'OK',
'201' => '已创建',
'202' => '已接受',
'204' => '无内容',
'400' => '错误请求',
'401' => '未授权',
'403' => '禁止访问',
'404' => '未找到',
'408' => '请求超时',
'422' => '无法处理的实体',
'429' => '请求过多',
'500' => '服务器内部错误',
'502' => '错误网关',
'503' => '服务不可用',
'504' => '网关超时',
],
'copied' => [
'id' => '已将 webhook ID 复制到剪贴板',
'secret' => '已将签名密钥复制到剪贴板',
'response' => '已复制响应正文',
'payload' => '已复制 payload',
],
'errors' => [
'endpoint_not_allowed' => '不允许使用此 endpoint。',
'endpoint_unreachable' => '无法访问该 endpoint。',
'endpoint_http_status' => '该 endpoint 返回了 HTTP :status。',
],
'flash' => [
'created' => '已创建 webhook。',
'updated' => '已更新 webhook。',
'deleted' => '已删除 webhook。',
'secret_rotated' => '已轮换签名密钥。',
'replayed' => '已重新发送 webhook 事件。',
'tested' => '已发送测试事件。',
],
'mail' => [
'paused_subject' => 'Webhook 已暂停::endpoint',
'paused_title' => 'Webhook 因连续失败已暂停',
'paused_preview' => '连续 5 次投递失败后,我们暂停了一个 webhook。',
'paused_body' => '连续 5 次投递失败后,我们暂停了 :endpoint 上的 webhook。请检查该 endpoint并在 webhook 详情页重新启用。',
'paused_cta' => '查看 webhook',
],
];

View file

@ -0,0 +1,34 @@
<x-main>
<div class="bg-zinc-50 sm:px-4 font-sans">
<table align="center">
<tr>
<td class="w-[552px] max-w-full">
<x-header />
<table class="w-full">
<tr>
<td class="p-12 sm:px-6 text-base text-zinc-700 bg-white rounded shadow-sm">
<h1 class="m-0 mb-6 text-2xl sm:leading-8 text-black font-semibold">
@{{ $title }}
</h1>
<p class="m-0 leading-6">
@{{ $body }}
</p>
<x-spacer height="24px" />
<div class="flex items-center justify-center">
<x-button href="@{{ $url }}">
@{{ $buttonText }} &rarr;
</x-button>
</div>
</td>
</tr>
</table>
<x-footer-authenticated />
</td>
</tr>
</table>
</div>
</x-main>

View file

@ -19,6 +19,7 @@ import {
IconPlugConnected, IconPlugConnected,
IconSelector, IconSelector,
IconTag, IconTag,
IconWebhook,
} from '@tabler/icons-vue'; } from '@tabler/icons-vue';
import { trans } from 'laravel-vue-i18n'; import { trans } from 'laravel-vue-i18n';
import { computed } from 'vue'; import { computed } from 'vue';
@ -57,6 +58,7 @@ import { portal } from '@/routes/app/billing';
import { index as labels } from '@/routes/app/labels'; import { index as labels } from '@/routes/app/labels';
import { index as mcp } from '@/routes/app/mcp'; import { index as mcp } from '@/routes/app/mcp';
import { index as signatures } from '@/routes/app/signatures'; import { index as signatures } from '@/routes/app/signatures';
import { index as webhooks } from '@/routes/app/webhooks';
import type { NavItem, User } from '@/types'; import type { NavItem, User } from '@/types';
interface Workspace { interface Workspace {
@ -80,6 +82,7 @@ const subscriptionPastDue = computed<boolean>(() =>
const { const {
canCreatePost, canCreatePost,
canManageAccounts, canManageAccounts,
canManageWebhooks,
canManageAutomations, canManageAutomations,
canCreateWorkspace, canCreateWorkspace,
} = useWorkspaceRole(); } = useWorkspaceRole();
@ -165,6 +168,15 @@ const workspaceNavItems = computed<NavItem[]>(() => [
}, },
] ]
: []), : []),
...(canManageWebhooks.value
? [
{
title: trans('sidebar.workspace.webhooks'),
href: webhooks.url(),
icon: IconWebhook,
},
]
: []),
{ {
title: trans('sidebar.workspace.mcp'), title: trans('sidebar.workspace.mcp'),
href: mcp.url(), href: mcp.url(),

View file

@ -4,6 +4,12 @@ import hljs from 'highlight.js/lib/core';
import jsonLang from 'highlight.js/lib/languages/json'; import jsonLang from 'highlight.js/lib/languages/json';
import { computed } from 'vue'; import { computed } from 'vue';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { copyToClipboard } from '@/lib/utils'; import { copyToClipboard } from '@/lib/utils';
hljs.registerLanguage('json', jsonLang); hljs.registerLanguage('json', jsonLang);
@ -26,17 +32,26 @@ const highlighted = computed(() => {
</script> </script>
<template> <template>
<div class="json-viewer overflow-hidden rounded-lg border-2 border-foreground"> <div class="json-viewer group relative overflow-hidden rounded-lg border-2 border-foreground">
<div class="flex items-center justify-end border-b-2 border-foreground/15 bg-card px-2 py-1.5"> <TooltipProvider v-if="serialized" :delay-duration="200">
<button <div
type="button" class="absolute right-2 top-2 z-10 opacity-0 transition-opacity duration-150 group-hover:opacity-100 focus-within:opacity-100"
class="inline-flex h-7 items-center gap-1.5 rounded-md border-2 border-foreground bg-card px-2 text-xs font-bold uppercase tracking-wider shadow-[1px_1px_0_var(--foreground)] transition hover:-translate-x-px hover:-translate-y-px hover:shadow-[2px_2px_0_var(--foreground)] active:translate-x-0 active:translate-y-0 active:shadow-[0_0_0_var(--foreground)]"
@click="copyToClipboard(serialized)"
> >
<IconCopy class="size-3.5" stroke-width="2.5" /> <Tooltip>
{{ $t('common.actions.copy') }} <TooltipTrigger as-child>
</button> <button
</div> type="button"
class="inline-flex size-7 items-center justify-center rounded-md border-2 border-foreground bg-card shadow-[1px_1px_0_var(--foreground)] transition hover:-translate-x-px hover:-translate-y-px hover:shadow-[2px_2px_0_var(--foreground)] active:translate-x-0 active:translate-y-0 active:shadow-[0_0_0_var(--foreground)]"
:aria-label="$t('common.actions.copy')"
@click="copyToClipboard(serialized)"
>
<IconCopy class="size-3.5" stroke-width="2.5" />
</button>
</TooltipTrigger>
<TooltipContent>{{ $t('common.actions.copy') }}</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
<pre class="json-viewer__body overflow-x-auto p-3 text-xs leading-relaxed"><code class="hljs language-json" v-html="highlighted" /></pre> <pre class="json-viewer__body overflow-x-auto p-3 text-xs leading-relaxed"><code class="hljs language-json" v-html="highlighted" /></pre>
</div> </div>
</template> </template>

View file

@ -1,55 +1,62 @@
<script setup lang="ts"> <script setup lang="ts">
import { usePage } from '@inertiajs/vue3'; import { usePage } from '@inertiajs/vue3';
import { onMounted, watch } from 'vue'; import { watch } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { Toaster } from '@/components/ui/sonner'; import { Toaster } from '@/components/ui/sonner';
interface Flash { type Flash = {
banner?: string; banner?: string;
bannerStyle?: 'success' | 'danger' | 'warning' | 'info'; bannerStyle?: 'success' | 'danger' | 'warning' | 'info';
success?: string; success?: string;
error?: string; error?: string;
warning?: string; warning?: string;
info?: string; info?: string;
} };
const page = usePage(); const page = usePage();
const showFlash = (flash: Flash | undefined) => {
if (!flash) return;
if (flash.banner) {
switch (flash.bannerStyle) {
case 'danger':
toast.error(flash.banner);
break;
case 'warning':
toast.warning(flash.banner);
break;
case 'info':
toast.info(flash.banner);
break;
case 'success':
default:
toast.success(flash.banner);
}
}
if (flash.success) toast.success(flash.success);
if (flash.error) toast.error(flash.error);
if (flash.warning) toast.warning(flash.warning);
if (flash.info) toast.info(flash.info);
};
onMounted(() => {
showFlash(page.props.flash as Flash | undefined);
});
watch( watch(
() => page.props.flash, () => page.props.flash as Flash | undefined,
(flash) => showFlash(flash as Flash | undefined), (flash) => {
{ deep: true }, if (!flash) {
return;
}
if (flash.banner) {
switch (flash.bannerStyle) {
case 'danger':
toast.error(flash.banner, { id: 'flash-banner' });
break;
case 'warning':
toast.warning(flash.banner, { id: 'flash-banner' });
break;
case 'info':
toast.info(flash.banner, { id: 'flash-banner' });
break;
case 'success':
default:
toast.success(flash.banner, { id: 'flash-banner' });
}
}
if (flash.success) {
toast.success(flash.success, { id: 'flash-success' });
}
if (flash.error) {
toast.error(flash.error, { id: 'flash-error' });
}
if (flash.warning) {
toast.warning(flash.warning, { id: 'flash-warning' });
}
if (flash.info) {
toast.info(flash.info, { id: 'flash-info' });
}
},
{ deep: true, immediate: true },
); );
</script> </script>

View file

@ -7,7 +7,6 @@ import {
IconRss, IconRss,
IconSend, IconSend,
IconSparkles, IconSparkles,
IconWebhook,
IconWorld, IconWorld,
} from '@tabler/icons-vue'; } from '@tabler/icons-vue';
import { trans } from 'laravel-vue-i18n'; import { trans } from 'laravel-vue-i18n';
@ -41,7 +40,6 @@ const categories = computed(() => [
title: trans('automations.categories.output'), title: trans('automations.categories.output'),
nodes: [ nodes: [
{ type: NodeType.Publish, label: trans('automations.nodes.publish'), icon: IconSend, accent: 'emerald' }, { type: NodeType.Publish, label: trans('automations.nodes.publish'), icon: IconSend, accent: 'emerald' },
{ type: NodeType.Webhook, label: trans('automations.nodes.webhook'), icon: IconWebhook, accent: 'slate' },
], ],
}, },
]); ]);

View file

@ -1,44 +0,0 @@
import { trans } from 'laravel-vue-i18n';
import { NodeType } from '@/types/automation/node-type';
interface WorkflowNode {
type?: string;
data?: Record<string, unknown> | null;
}
/**
* Mirrors the backend WebhookNodeValidator: a webhook payload template must be
* valid JSON because the runtime parses it before resolving `{{ }}` placeholders.
* An empty or literal-`null` template means "no body" and is valid.
*/
export const isPayloadTemplateValid = (template: string): boolean => {
const trimmed = template.trim();
if (trimmed === '' || trimmed === 'null') {
return true;
}
try {
JSON.parse(trimmed);
return true;
} catch {
return false;
}
};
/**
* First node-config issue that would block a run, or null when every node is
* runnable. The frontend gate intentionally covers only the Webhook node the
* Generate node has its own inline compliance UI, and the backend
* AutomationConfigValidator remains the safety net for everything.
*/
export const firstConfigIssue = (nodes: WorkflowNode[]): string | null => {
for (const node of nodes) {
if (node.type === NodeType.Webhook && !isPayloadTemplateValid(String(node.data?.payload_template ?? ''))) {
return trans('automations.errors.webhook_invalid_payload_json');
}
}
return null;
};

View file

@ -1,84 +0,0 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { isPayloadTemplateValid } from '@/components/automations/config-validation';
import CodeEditor from '@/components/CodeEditor.vue';
import InputError from '@/components/InputError.vue';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useExpandedEditor } from '@/composables/useExpandedEditor';
import { HTTP_METHODS, HttpMethod, type HttpMethodValue } from '@/types/automation/http-method';
interface WebhookConfig {
url: string;
method: HttpMethodValue;
headers?: Record<string, string>;
payload_template: string;
}
const props = defineProps<{
data: Record<string, unknown>;
errors?: Record<string, string>;
}>();
const emit = defineEmits<{ update: [Record<string, unknown>] }>();
const editorExpanded = useExpandedEditor();
const local = ref<WebhookConfig>({
url: (props.data.url as string) ?? '',
method: (props.data.method as WebhookConfig['method']) ?? HttpMethod.Post,
headers: (props.data.headers as Record<string, string>) ?? {},
payload_template: (props.data.payload_template as string) ?? '{}',
});
watch(local, (val) => emit('update', val), { deep: true });
const isPayloadJsonInvalid = computed(() => !isPayloadTemplateValid(local.value.payload_template));
</script>
<template>
<div class="space-y-3">
<div>
<Label class="mb-1 block">{{ $t('automations.config.webhook.url') }}</Label>
<Input v-model="local.url" placeholder="https://hooks.example.com/…" />
<InputError :message="errors?.url" class="mt-1" />
</div>
<div>
<Label class="mb-1 block">{{ $t('automations.config.webhook.method') }}</Label>
<Select v-model="local.method">
<SelectTrigger class="w-full">
<SelectValue :placeholder="$t('automations.config.select_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="m in HTTP_METHODS" :key="m" :value="m">{{ m }}</SelectItem>
</SelectContent>
</Select>
<InputError :message="errors?.method" class="mt-1" />
</div>
<div v-show="!editorExpanded">
<Label class="mb-1 block">{{ $t('automations.config.webhook.payload_template') }}</Label>
<div class="h-40">
<CodeEditor
v-model="local.payload_template"
language="json"
expandable
:label="$t('automations.config.webhook.payload_template')"
placeholder='{"content": "{{ post.content }}"}'
/>
</div>
<p v-if="isPayloadJsonInvalid" class="mt-1 text-xs text-amber-600 dark:text-amber-500">
{{ $t('automations.config.invalid_json') }}
</p>
<InputError :message="errors?.payload_template" class="mt-1" />
</div>
</div>
</template>

View file

@ -1,48 +0,0 @@
<script setup lang="ts">
import { IconWebhook } from '@tabler/icons-vue';
import { Handle, Position } from '@vue-flow/core';
import { computed } from 'vue';
import type { HttpMethodValue } from '@/types/automation/http-method';
const props = defineProps<{
data: {
url?: string;
method: HttpMethodValue;
};
selected?: boolean;
}>();
const summary = computed(() => {
const method = props.data.method.toUpperCase();
const url = props.data.url || 'https://…';
return `${method} · ${url}`;
});
</script>
<template>
<div
class="automation-node automation-node--wide automation-node--accent-slate"
:class="{ 'is-selected': selected }"
>
<div class="automation-node__header">
<div class="automation-node__icon-tile automation-node__icon-tile--slate">
<IconWebhook :size="16" />
</div>
<span class="automation-node__title">{{ $t('automations.nodes.webhook') }}</span>
</div>
<div class="automation-node__summary" :title="summary">
{{ summary }}
</div>
<Handle
type="target"
:position="Position.Left"
class="!bg-slate-500"
/>
<Handle
type="source"
:position="Position.Right"
class="!bg-slate-500"
/>
</div>
</template>

View file

@ -0,0 +1,80 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { watch } from 'vue';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { store } from '@/routes/app/webhooks';
import WebhookFormFields from './WebhookFormFields.vue';
const open = defineModel<boolean>('open', { default: false });
const form = useForm({
endpoint: '',
events: [] as string[],
});
watch(open, (isOpen) => {
if (isOpen) {
form.reset();
form.clearErrors();
}
});
const submit = () => {
form.post(store.url(), {
onSuccess: () => {
open.value = false;
},
});
};
</script>
<template>
<Dialog v-model:open="open">
<DialogContent class="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{{ $t('webhooks.create.title') }}</DialogTitle>
<DialogDescription>
{{ $t('webhooks.create.description') }}
</DialogDescription>
</DialogHeader>
<form class="space-y-4" @submit.prevent="submit">
<WebhookFormFields
v-model:endpoint="form.endpoint"
v-model:events="form.events"
endpoint-id="create-endpoint"
endpoint-test-id="create-webhook-endpoint"
events-test-id="create-webhook-events"
:errors="form.errors"
/>
<DialogFooter>
<Button
type="submit"
data-testid="create-webhook-submit"
:disabled="form.processing || form.events.length === 0"
>
{{ $t('webhooks.create.submit') }}
</Button>
<Button
variant="secondary"
type="button"
data-testid="cancel-create-webhook"
@click="open = false"
>
{{ $t('webhooks.create.cancel') }}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</template>

View file

@ -0,0 +1,91 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { watch } from 'vue';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { update } from '@/routes/app/webhooks';
import WebhookFormFields from './WebhookFormFields.vue';
interface WebhookItem {
id: string;
endpoint: string;
events: string[];
}
const props = defineProps<{
webhook: WebhookItem;
}>();
const open = defineModel<boolean>('open', { default: false });
const form = useForm({
endpoint: props.webhook.endpoint,
events: [...props.webhook.events],
});
watch(open, (isOpen) => {
if (isOpen) {
form.endpoint = props.webhook.endpoint;
form.events = [...props.webhook.events];
form.clearErrors();
}
});
const submit = () => {
form.put(update.url(props.webhook), {
onSuccess: () => {
open.value = false;
},
});
};
</script>
<template>
<Dialog v-model:open="open">
<DialogContent class="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{{ $t('webhooks.edit.title') }}</DialogTitle>
<DialogDescription>
{{ $t('webhooks.edit.description') }}
</DialogDescription>
</DialogHeader>
<form class="space-y-4" @submit.prevent="submit">
<WebhookFormFields
v-model:endpoint="form.endpoint"
v-model:events="form.events"
endpoint-id="edit-endpoint"
endpoint-test-id="edit-webhook-endpoint"
events-test-id="edit-webhook-events"
:errors="form.errors"
/>
<DialogFooter>
<Button
type="submit"
data-testid="edit-webhook-submit"
:disabled="form.processing || form.events.length === 0"
>
{{ $t('webhooks.edit.submit') }}
</Button>
<Button
variant="secondary"
type="button"
data-testid="cancel-edit-webhook"
@click="open = false"
>
{{ $t('webhooks.edit.cancel') }}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</template>

View file

@ -0,0 +1,62 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { ref } from 'vue';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { rotateSecret } from '@/routes/app/webhooks';
const props = defineProps<{
webhookId: string;
}>();
const open = defineModel<boolean>('open', { default: false });
const rotating = ref(false);
const handleRotate = () => {
rotating.value = true;
router.post(
rotateSecret.url(props.webhookId),
{},
{
preserveScroll: true,
onFinish: () => {
rotating.value = false;
open.value = false;
},
},
);
};
</script>
<template>
<Dialog v-model:open="open">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ $t('webhooks.rotate.title') }}</DialogTitle>
<DialogDescription>
{{ $t('webhooks.rotate.description') }}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
data-testid="rotate-secret-submit"
:disabled="rotating"
@click="handleRotate"
>
{{ $t('webhooks.rotate.submit') }}
</Button>
<Button variant="outline" @click="open = false">
{{ $t('webhooks.rotate.cancel') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

View file

@ -0,0 +1,127 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import {
IconCopy,
IconDots,
IconPencil,
IconPlayerPause,
IconPlayerPlay,
IconRefresh,
IconSend,
IconTrash,
} from '@tabler/icons-vue';
import { trans } from 'laravel-vue-i18n';
import { ref } from 'vue';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { copyToClipboard } from '@/lib/utils';
import { sendTest, update } from '@/routes/app/webhooks';
import type { Webhook } from '@/types/webhook';
import { WebhookStatus } from '@/types/webhook-status';
const props = defineProps<{
webhook: Webhook;
}>();
const emit = defineEmits<{
edit: [];
rotate: [];
delete: [];
}>();
const togglingStatus = ref(false);
const sendingTest = ref(false);
const sendTestEvent = () => {
sendingTest.value = true;
router.post(sendTest.url(props.webhook), {}, {
preserveScroll: true,
onFinish: () => {
sendingTest.value = false;
},
});
};
const toggleStatus = () => {
togglingStatus.value = true;
router.put(
update.url(props.webhook),
{
status:
props.webhook.status === WebhookStatus.Enabled
? WebhookStatus.Disabled
: WebhookStatus.Enabled,
},
{
preserveScroll: true,
onFinish: () => {
togglingStatus.value = false;
},
},
);
};
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" size="icon" data-testid="webhook-actions-trigger">
<IconDots class="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem data-testid="edit-webhook-button" @click="emit('edit')">
<IconPencil class="size-4" />
{{ $t('webhooks.actions.edit') }}
</DropdownMenuItem>
<DropdownMenuItem :disabled="togglingStatus" @click="toggleStatus">
<IconPlayerPlay v-if="webhook.status !== WebhookStatus.Enabled" class="size-4" />
<IconPlayerPause v-else class="size-4" />
{{
webhook.status === WebhookStatus.Enabled
? $t('webhooks.actions.disable')
: $t('webhooks.actions.enable')
}}
</DropdownMenuItem>
<DropdownMenuItem @click="emit('rotate')">
<IconRefresh class="size-4" />
{{ $t('webhooks.actions.rotate') }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
data-testid="send-test-webhook"
:disabled="sendingTest"
@click="sendTestEvent"
>
<IconSend class="size-4" />
{{ $t('webhooks.actions.send_test') }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
data-testid="copy-id-button"
@click="copyToClipboard(webhook.id, trans('webhooks.copied.id'))"
>
<IconCopy class="size-4" />
{{ $t('webhooks.actions.copy_id') }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
data-testid="delete-webhook-button"
@click="emit('delete')"
>
<IconTrash class="size-4" />
{{ $t('webhooks.actions.delete') }}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

Some files were not shown because too many files have changed in this diff Show more