2026-05-24 12:17:19 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace App\Actions\Automation\Node;
|
|
|
|
|
|
Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
graph-aware (suggests only what upstream nodes provide + variables + now);
migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
(matching the sidebar card), with a minimize control; the inline field
collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form
HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
dates; first poll records a baseline and emits nothing (date path too)
Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
so the test panel aggregates all branches instead of one
Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 14:31:58 +00:00
|
|
|
use App\Actions\Automation\Run\AdvanceAutomationRun;
|
2026-05-24 12:17:19 +00:00
|
|
|
use App\DataTransferObjects\Automation\NodeRunResult;
|
|
|
|
|
use App\Enums\Automation\Run\Status as RunStatus;
|
|
|
|
|
use App\Models\AutomationNodeState;
|
|
|
|
|
use App\Models\AutomationRun;
|
2026-06-11 18:47:29 +00:00
|
|
|
use App\Services\Automation\ExpressionResolver;
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
use App\Services\Automation\FeedParser;
|
2026-06-11 18:47:29 +00:00
|
|
|
use App\Services\Brand\SafeHttpFetcher;
|
2026-05-24 12:17:19 +00:00
|
|
|
use Carbon\CarbonImmutable;
|
2026-06-11 18:47:29 +00:00
|
|
|
use RuntimeException;
|
2026-05-24 12:17:19 +00:00
|
|
|
use Throwable;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetches an RSS feed and progresses the run with the next-up unseen item.
|
|
|
|
|
*
|
|
|
|
|
* - Filters by a per-node watermark stored in `automation_node_states`, so the
|
|
|
|
|
* very first run on an old feed processes none of the historical items.
|
|
|
|
|
* - 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
|
|
|
|
|
* immediately after this Fetch (with `context.fetched` already populated),
|
|
|
|
|
* so each item ends up generating its own Post / Webhook / etc.
|
|
|
|
|
* - 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
|
|
|
|
|
* completes silently (handled by AdvanceAutomationRun's default branch).
|
|
|
|
|
*/
|
|
|
|
|
class RunFetchRssNode
|
|
|
|
|
{
|
2026-06-10 23:45:01 +00:00
|
|
|
private const ITEM_HANDLE = 'default';
|
|
|
|
|
|
Back fixed-set automation strings with enums and consts
Replace magic strings across the automation domain with backed PHP enums
(HttpMethod, AuthType, DelayUnit, ScheduleField) and mirrored TS consts
(http-method, auth-type, delay-unit, schedule-field, condition-operator,
publish-mode), plus the existing Condition\Handle / Operator / Publish\Mode.
Also:
- require scheduled_offset via concrete-index required_if instead of
defaulting to 60 when the publish mode is scheduled
- fail the webhook node explicitly when the resolved url is empty
- localize node failure messages (fetch_rss/http/webhook)
- cast resolver/strtoupper inputs to string so a present-null config value
degrades gracefully instead of crashing
- list automations with config('app.pagination.default'), drop the perPage param
2026-06-13 18:04:24 +00:00
|
|
|
private const NO_ITEMS_HANDLE = 'no_items';
|
|
|
|
|
|
2026-06-11 18:47:29 +00:00
|
|
|
public function __construct(
|
|
|
|
|
private ExpressionResolver $resolver,
|
|
|
|
|
private SafeHttpFetcher $safeHttp,
|
Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
graph-aware (suggests only what upstream nodes provide + variables + now);
migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
(matching the sidebar card), with a minimize control; the inline field
collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form
HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
dates; first poll records a baseline and emits nothing (date path too)
Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
so the test panel aggregates all branches instead of one
Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 14:31:58 +00:00
|
|
|
private AdvanceAutomationRun $advance,
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
private FeedParser $parser,
|
2026-06-11 18:47:29 +00:00
|
|
|
) {}
|
|
|
|
|
|
2026-05-24 12:17:19 +00:00
|
|
|
public function __invoke(AutomationRun $run, array $config): NodeRunResult
|
|
|
|
|
{
|
2026-06-11 18:47:29 +00:00
|
|
|
$feedUrl = $this->resolver->resolve((string) data_get($config, 'feed_url', ''), $run->resolverContext());
|
2026-05-24 12:17:19 +00:00
|
|
|
|
|
|
|
|
if ($feedUrl === '') {
|
2026-06-13 16:59:52 +00:00
|
|
|
return NodeRunResult::failed(__('automations.errors.fetch_rss_missing_url'));
|
2026-05-24 12:17:19 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-17 17:55:56 +00:00
|
|
|
// SafeHttpFetcher::get() re-validates every redirect hop against the SSRF
|
|
|
|
|
// guard (not just the initial URL), so a public feed that 302s to an
|
|
|
|
|
// internal host is never followed. It throws on a blocked hop, connection
|
|
|
|
|
// failure, non-2xx status, or an excessive redirect chain — all of which
|
|
|
|
|
// are legitimate "this feed couldn't be fetched" failures for this node.
|
2026-06-11 18:47:29 +00:00
|
|
|
try {
|
2026-07-17 17:55:56 +00:00
|
|
|
$response = $this->safeHttp->get($feedUrl);
|
|
|
|
|
} catch (RuntimeException $e) {
|
|
|
|
|
return NodeRunResult::failed(__('automations.errors.fetch_rss_request_failed'), [
|
|
|
|
|
'message' => $e->getMessage(),
|
2026-06-11 18:47:29 +00:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
$items = $this->parser->parse($response->body());
|
|
|
|
|
|
|
|
|
|
if ($items === null) {
|
2026-06-13 16:59:52 +00:00
|
|
|
return NodeRunResult::failed(__('automations.errors.fetch_rss_malformed'));
|
2026-05-24 12:17:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$nodeId = (string) $run->current_node_id;
|
Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
graph-aware (suggests only what upstream nodes provide + variables + now);
migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
(matching the sidebar card), with a minimize control; the inline field
collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form
HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
dates; first poll records a baseline and emits nothing (date path too)
Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
so the test panel aggregates all branches instead of one
Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 14:31:58 +00:00
|
|
|
// Test runs (dry OR real-data) bypass the watermark entirely: they use an
|
|
|
|
|
// epoch watermark so every item is treated as new, process only the first
|
|
|
|
|
// item, and never spawn siblings or advance the watermark. This way a
|
|
|
|
|
// test ALWAYS shows real data flowing through (instead of "no new items")
|
|
|
|
|
// and never floods the feed or poisons the production watermark.
|
|
|
|
|
$isPreview = $run->is_manual || $run->is_dry_run;
|
|
|
|
|
$state = $isPreview ? null : AutomationNodeState::for($run->automation_id, $nodeId);
|
|
|
|
|
$watermark = $isPreview
|
2026-05-24 12:17:19 +00:00
|
|
|
? CarbonImmutable::createFromTimestamp(0)
|
|
|
|
|
: $this->parseWatermark(data_get($state->data, 'last_item_date'));
|
|
|
|
|
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
[$newItems, $newestSeen] = $this->collectNewItems($items, $watermark);
|
2026-05-24 12:17:19 +00:00
|
|
|
|
Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
graph-aware (suggests only what upstream nodes provide + variables + now);
migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
(matching the sidebar card), with a minimize control; the inline field
collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form
HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
dates; first poll records a baseline and emits nothing (date path too)
Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
so the test panel aggregates all branches instead of one
Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 14:31:58 +00:00
|
|
|
if ($state !== null && $newestSeen !== null) {
|
2026-05-24 12:17:19 +00:00
|
|
|
$state->update(['data' => array_merge($state->data ?? [], [
|
|
|
|
|
'last_item_date' => $newestSeen->toIso8601String(),
|
|
|
|
|
])]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($newItems === []) {
|
Back fixed-set automation strings with enums and consts
Replace magic strings across the automation domain with backed PHP enums
(HttpMethod, AuthType, DelayUnit, ScheduleField) and mirrored TS consts
(http-method, auth-type, delay-unit, schedule-field, condition-operator,
publish-mode), plus the existing Condition\Handle / Operator / Publish\Mode.
Also:
- require scheduled_offset via concrete-index required_if instead of
defaulting to 60 when the publish mode is scheduled
- fail the webhook node explicitly when the resolved url is empty
- localize node failure messages (fetch_rss/http/webhook)
- cast resolver/strtoupper inputs to string so a present-null config value
degrades gracefully instead of crashing
- list automations with config('app.pagination.default'), drop the perPage param
2026-06-13 18:04:24 +00:00
|
|
|
return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: self::NO_ITEMS_HANDLE);
|
2026-05-24 12:17:19 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-16 14:26:05 +00:00
|
|
|
$total = count($newItems);
|
|
|
|
|
|
|
|
|
|
// A preview (manual/dry test) surfaces ONE item and never fans out, so it
|
|
|
|
|
// shows the newest item — what the user expects to test against. A real run
|
|
|
|
|
// takes the oldest new item and fans the rest out as siblings, preserving
|
|
|
|
|
// feed chronology across branches (items are sorted oldest-first).
|
|
|
|
|
if ($isPreview) {
|
|
|
|
|
return NodeRunResult::completed([
|
|
|
|
|
'fetch' => ['count' => $total, 'spawned' => 0],
|
|
|
|
|
'fetched' => end($newItems),
|
|
|
|
|
]);
|
2026-05-24 12:17:19 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-16 14:26:05 +00:00
|
|
|
$first = array_shift($newItems);
|
|
|
|
|
$this->spawnSiblings($run, $nodeId, $newItems);
|
|
|
|
|
|
2026-05-24 12:17:19 +00:00
|
|
|
return NodeRunResult::completed([
|
2026-06-16 14:26:05 +00:00
|
|
|
'fetch' => ['count' => $total, 'spawned' => count($newItems)],
|
2026-05-24 12:17:19 +00:00
|
|
|
'fetched' => $first,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
/**
|
|
|
|
|
* @param list<array<string, mixed>> $parsed Normalized items from FeedParser.
|
|
|
|
|
* @return array{0: list<array<string, mixed>>, 1: ?CarbonImmutable}
|
|
|
|
|
*/
|
|
|
|
|
private function collectNewItems(array $parsed, CarbonImmutable $watermark): array
|
2026-05-24 12:17:19 +00:00
|
|
|
{
|
|
|
|
|
$items = [];
|
|
|
|
|
$newestSeen = null;
|
|
|
|
|
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
foreach ($parsed as $item) {
|
|
|
|
|
$key = (string) data_get($item, 'key', '');
|
2026-05-24 12:17:19 +00:00
|
|
|
if ($key === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
$date = $this->parsePubDate((string) data_get($item, 'date', ''));
|
|
|
|
|
if ($date === null) {
|
2026-05-24 12:17:19 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
if ($newestSeen === null || $date->greaterThan($newestSeen)) {
|
|
|
|
|
$newestSeen = $date;
|
2026-05-24 12:17:19 +00:00
|
|
|
}
|
|
|
|
|
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
if (! $date->greaterThan($watermark)) {
|
2026-05-24 12:17:19 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
$item['_sort'] = $date->getTimestamp();
|
|
|
|
|
$items[] = $item;
|
2026-05-24 12:17:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process oldest-first so siblings inherit a stable order matching feed chronology.
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
usort($items, fn ($a, $b) => $a['_sort'] <=> $b['_sort']);
|
2026-05-24 12:17:19 +00:00
|
|
|
|
|
|
|
|
// Drop the internal sort key — downstream nodes shouldn't see it.
|
|
|
|
|
$items = array_map(function (array $item): array {
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
unset($item['_sort']);
|
2026-05-24 12:17:19 +00:00
|
|
|
|
|
|
|
|
return $item;
|
|
|
|
|
}, $items);
|
|
|
|
|
|
|
|
|
|
return [$items, $newestSeen];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function spawnSiblings(AutomationRun $parent, string $fetchNodeId, array $items): void
|
|
|
|
|
{
|
|
|
|
|
if ($items === []) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
graph-aware (suggests only what upstream nodes provide + variables + now);
migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
(matching the sidebar card), with a minimize control; the inline field
collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form
HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
dates; first poll records a baseline and emits nothing (date path too)
Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
so the test panel aggregates all branches instead of one
Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 14:31:58 +00:00
|
|
|
// Each remaining item gets its own run that fans out across EVERY branch
|
|
|
|
|
// wired to the fetch node — matching how item[0] (the current run) fans
|
|
|
|
|
// out, so no branch silently drops items 2..N.
|
|
|
|
|
$targets = $this->advance->targetsFor($parent->automation, $fetchNodeId, self::ITEM_HANDLE);
|
2026-05-24 12:17:19 +00:00
|
|
|
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
$sibling = AutomationRun::create([
|
|
|
|
|
'automation_id' => $parent->automation_id,
|
Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
graph-aware (suggests only what upstream nodes provide + variables + now);
migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
(matching the sidebar card), with a minimize control; the inline field
collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form
HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
dates; first poll records a baseline and emits nothing (date path too)
Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
so the test panel aggregates all branches instead of one
Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 14:31:58 +00:00
|
|
|
'root_run_id' => $parent->rootId(),
|
|
|
|
|
'trigger_item_id' => $parent->trigger_item_id,
|
|
|
|
|
'generated_post_id' => $parent->generated_post_id,
|
2026-05-24 12:17:19 +00:00
|
|
|
'is_manual' => $parent->is_manual,
|
|
|
|
|
'is_dry_run' => $parent->is_dry_run,
|
|
|
|
|
'status' => RunStatus::Pending,
|
|
|
|
|
'context' => array_merge($parent->context ?? [], ['fetched' => $item]),
|
|
|
|
|
]);
|
|
|
|
|
|
Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
graph-aware (suggests only what upstream nodes provide + variables + now);
migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
(matching the sidebar card), with a minimize control; the inline field
collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form
HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
dates; first poll records a baseline and emits nothing (date path too)
Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
so the test panel aggregates all branches instead of one
Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 14:31:58 +00:00
|
|
|
if ($targets === []) {
|
2026-05-24 12:17:19 +00:00
|
|
|
$sibling->update(['status' => RunStatus::Completed, 'finished_at' => now()]);
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
graph-aware (suggests only what upstream nodes provide + variables + now);
migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
(matching the sidebar card), with a minimize control; the inline field
collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form
HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
dates; first poll records a baseline and emits nothing (date path too)
Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
so the test panel aggregates all branches instead of one
Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 14:31:58 +00:00
|
|
|
$this->advance->dispatchBranches($sibling, $targets);
|
2026-05-24 12:17:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function parseWatermark(?string $stored): CarbonImmutable
|
|
|
|
|
{
|
|
|
|
|
if ($stored === null) {
|
|
|
|
|
return CarbonImmutable::now();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return CarbonImmutable::parse($stored);
|
|
|
|
|
} catch (Throwable) {
|
|
|
|
|
return CarbonImmutable::now();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function parsePubDate(string $raw): ?CarbonImmutable
|
|
|
|
|
{
|
|
|
|
|
if ($raw === '') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return CarbonImmutable::parse($raw);
|
|
|
|
|
} catch (Throwable) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|