Commit graph

689 commits

Author SHA1 Message Date
Paulo Castellano
2585d89cb4 Address PR review: connect-status enum, publisher cleanup, fill test gaps 2026-06-13 22:38:23 -03:00
Paulo Castellano
deb1c6fa69 Extract Telegram webhook registration into an action 2026-06-13 22:20:52 -03:00
Paulo Castellano
f378469842 Wire Telegram into the post composer (preview, content type, logo) 2026-06-13 22:10:12 -03:00
Paulo Castellano
b504243212 Document Telegram env vars in docker .env example 2026-06-13 22:02:02 -03:00
Paulo Castellano
b6605ea228 Document Telegram env vars in .env.example 2026-06-13 22:01:37 -03:00
Paulo Castellano
79acdccc45 Add Telegram connection UI (connect dialog + code polling) 2026-06-13 21:58:49 -03:00
Paulo Castellano
a4cf8aa4ce Add Telegram connection flow (controller + webhook)
Connect a channel by issuing a one-time code the user posts as /connect <code>
in their channel. A secret-token-guarded webhook matches the code, links the
channel as a SocialAccount (chat_id in meta), and records it on the request so
the connect endpoint can poll for completion. Adds the TelegramConnectRequest
model + migration, the connect/status endpoints, the public webhook route (CSRF
exempt), a ConnectionVerifier branch (getChat liveness), and a telegram:set-webhook
command. Tests cover the code issue, webhook link, secret rejection, expired/
unknown codes, status polling, and the command.
2026-06-13 21:39:03 -03:00
Paulo Castellano
9634e88e5d Add Telegram publishing (backend foundation)
Register Telegram as a platform: Platform/ContentType enum cases, a
platforms.telegram config block (shared bot token via env), TelegramPublisher
(sendMessage / sendPhoto|Video|Document / sendMediaGroup over the Bot API, HTML
parse mode, 4096 limit with long text split off a 1024 caption), wired into the
publisher dispatch. Add a Telegram ContentSanitizer branch (Telegram-allowed
HTML + ampersand escaping), MediaOptimizer/profile-url/factory support, and the
TelegramPublishException. Tests cover text, single media, album, long-text split,
overflow, API errors, private-channel URLs, and sanitization.
2026-06-13 21:25:31 -03:00
Paulo Castellano
986e2661f9
Merge pull request #94 from trypostit/fix/bluesky-publisher-hardening
Harden BlueskyPublisher: link facets, conventions, NSID lexicon, tests
2026-06-13 20:38:09 -03:00
Paulo Castellano
9864ea96b6 Cover handle-resolution errors and configured web-app host
Add a test that a network error during handle resolution degrades the mention
to plain text instead of failing the post (exercises the resolveHandleToDid
try/catch), and a test asserting the post URL is built from the configured
web_app host (a revert to a hardcoded host would now fail).
2026-06-13 20:30:41 -03:00
Paulo Castellano
444d522012 Lock lexicon values and the URL paren-keep branch in tests
Add a unit test asserting each BlueskyLexicon constant equals its AT Protocol
NSID — a wrong value (not just a typo'd name) would otherwise fail silently at
runtime. Add a test that a URL with a matching '(' keeps its trailing ')'
(Wikipedia-style), covering the previously-untested keep branch of the trim.
2026-06-13 20:25:05 -03:00
Paulo Castellano
8cfdd4c128 Centralize AT Protocol NSIDs in BlueskyLexicon
The Bluesky lexicon identifiers (createRecord, createSession, feed.post, facet
types, etc.) were repeated as magic strings across BlueskyPublisher,
BlueskyAnalytics, ConnectionVerifier and BlueskyController, where a typo fails
silently at runtime as "Invalid request". Define them once as named constants
so a typo is an undefined-constant error instead. Tests keep the literal NSIDs
as the independent contract.
2026-06-13 20:18:39 -03:00
Paulo Castellano
c86596ada3 Assert image embed and 4-image cap in tests
The image tests only checked that createRecord was sent — the faked empty
download meant no blob was ever attached, so they never verified the embed.
Drive a real (mocked) optimize + upload and assert the createRecord record
carries an app.bsky.embed.images embed, and that six images upload exactly four.
2026-06-13 20:07:25 -03:00
Paulo Castellano
84052a2f1c Tidy BlueskyPublisher conventions
Import Exception instead of referencing it inline, read the blob via data_get,
source the web-app host from config (new bluesky.web_app key) instead of
hardcoding bsky.app, and drop the getUtf8ByteOffset no-op — the PREG /u offset
is already in bytes, so the call sites use it directly.
2026-06-13 20:04:40 -03:00
Paulo Castellano
2ff34369e0 Stop URL facets swallowing trailing punctuation
The URL regex captured trailing sentence punctuation and an unmatched closing
paren ("see https://x.com).") into the link facet, so Bluesky rendered the
punctuation as part of a malformed link. Trim it like the official atproto
tokenizer. Add tests for the trim and for byte-offset correctness after
multibyte characters (emoji/accents).
2026-06-13 20:02:54 -03:00
Paulo Castellano
7baf1b7df5
Merge pull request #93 from Schrall/fix/bluesky-mention-facet-resolve-did
Fix: resolve Bluesky @mention handles to DIDs (mentions caused "Invalid post data")
2026-06-13 19:57:17 -03:00
Paulo Castellano
60094c9316 Simplify handle resolution to a single AppView call
resolveHandle is a public read served by the AppView, so drop the PDS-first
auth, the multi-endpoint fallback loop and the threaded service/account args in
favor of one unauthenticated GET against config public_appview — matching how
BlueskyAnalytics already reads from the AppView. The try/catch stays so a
network error degrades the mention to plain text instead of failing the post.
Remove the now-obsolete fallback test.
2026-06-13 18:41:43 -03:00
Paulo Castellano
5227cde5c7 Cache unresolvable handles and tighten offset types
Memoize by key (array_key_exists) so a handle that fails to resolve is tried
once per post instead of on every repeat occurrence, and add a test that a
repeated handle resolves once. Cast the PREG_OFFSET_CAPTURE offsets to int to
match getUtf8ByteOffset's signature, and reword the resolver docblock to say
"configured default service" instead of naming bsky.social.
2026-06-13 18:35:30 -03:00
Paulo Castellano
9f0c264c39 Cover mention endpoint fallback and partial resolution
Add tests for the two untested branches of the DID resolution: falling back to
the public AppView when the account's PDS can't resolve a handle, and a post
mixing a resolvable mention (kept as a facet) with an unresolvable one (a 200
without a DID, left as plain text). The prior tests only hit the happy path and
the all-endpoints-fail path via a wildcard that masked the fallback.
2026-06-13 18:21:16 -03:00
Paulo Castellano
409c6f0139 Drop the resolution warning log and fake createRecord via config
Remove the final Log::warning (and the lastError/lastEndpoint tracking that only
fed it) — the per-endpoint debug log already covers diagnosis. Fake the
createRecord endpoint from config('trypost.platforms.bluesky.default_service')
in the new tests instead of hardcoding the host.
2026-06-13 18:19:04 -03:00
Paulo Castellano
5d80caffef Shorten the mention-facet comment to one line 2026-06-13 18:16:24 -03:00
Paulo Castellano
5cafac34c1 Resolve Bluesky endpoints from config and import Throwable
Use the existing config('trypost.platforms.bluesky.public_appview') and
default_service keys for the handle-resolution fallbacks instead of hardcoding
the hosts, so a self-hosted override (BLUESKY_PUBLIC_APPVIEW) is honored here
like it already is in BlueskyAnalytics. Import Throwable instead of referencing
it inline.
2026-06-13 18:15:31 -03:00
Paulo Castellano
2641cc66fe
Merge branch 'main' into fix/bluesky-mention-facet-resolve-did 2026-06-13 18:09:00 -03:00
Paulo Castellano
44fb42b500
Merge pull request #88 from trypostit/feat/automations-module
Automations ⚙️
2026-06-13 18:08:04 -03:00
Paulo Castellano
757a215d0d Back run/node status and node type with TS consts
Add NodeRunStatus and RunStatus consts mirroring the backend enums, and route
the test panel and invocations list through them (plus the existing NodeType)
instead of bare 'completed'/'failed'/'fetch_rss' string checks. Type the run/
node-run interfaces with the value unions, extract the status tile classes into
a helper, and render the node type via i18n like the invocations list already does.
2026-06-13 17:50:23 -03:00
Paulo Castellano
19f331741c Use Inertia's useHttp for automation JSON calls
The test-run and invocation-detail requests used raw fetch with a hand-rolled
CSRF header. Switch to Inertia v3's useHttp (the standalone XHR client already
used across the app), which handles CSRF, headers and error payloads — dropping
the manual plumbing and reading the 422 message from error.response.data.
2026-06-13 17:41:48 -03:00
Paulo Castellano
cf3cbfba9e Simplify the cron-to-schedule inference
Rewrite inferScheduleFromCron: bail once on a pinned month, group the branches
by cadence (sub-hourly / hourly / daily / weekly / monthly), and share the
hour+minute via one `clock` object instead of repeating the same guards in
eight stacked conditionals. Behavior-preserving — verified the generate↔infer
round-trip across every preset.
2026-06-13 17:31:02 -03:00
Paulo Castellano
c79b4a4a84 Type node enum props with their value unions
The required method/unit/operator/mode props were typed as plain string after
dropping their defaults. Use the matching *Value union types (HttpMethodValue,
DelayUnitValue, ConditionOperatorValue, PublishModeValue) so the renderers are
constrained to real enum values, matching the config panels.
2026-06-13 17:15:20 -03:00
Paulo Castellano
1ac4795575 Drop redundant method default in webhook/http nodes
Webhook and HTTP nodes are always created with a method, so the ?? default was
dead. Make the prop required and remove the now-unused HttpMethod import,
matching the unit/operator/mode cleanup.
2026-06-13 17:13:32 -03:00
Paulo Castellano
9b3e9e8307 Translate node summary labels in the canvas
The Generate/Delay/Condition/Publish node summaries rendered English prose and
raw enum values (account/accounts, single, hours, contains, scheduled · +N min).
Route them through i18n: pluralize the account count, and reuse the existing
unit/operator/mode/format labels in all three locales. Drop the redundant
?? defaults where the node is always created with the value (unit, operator,
mode) and tighten those prop types to required.
2026-06-13 17:10:38 -03:00
Paulo Castellano
90e9e4bee6 Centralize timezone display in the date helper
Add date.getTimezoneAbbr() and route every "GMT-x" abbreviation through it,
removing the three inline dayjs().format('z') copies (schedule-summary,
TriggerNodeConfig, PickTimePopover) and the duplicate userTimezone helper.
Drop the pointless computed wrapper for a value that never changes, and rename
snap5 to snapToFiveMinutes.
2026-06-13 16:46:34 -03:00
Paulo Castellano
ff8fd9de40 Tidy automation frontend: extract CSS, use the Label component
- Move the automation-node and json-viewer styles out of app.css into their
  own imported stylesheets.
- Replace raw <label> elements across the node config panels and the test
  panel with the shared ui/label component.
2026-06-13 16:30:39 -03:00
Paulo Castellano
730cb9d156 Tidy automation backend: run duration, folded migrations, imports
- Add AutomationRun::durationInMilliseconds() as the single source of truth
  for the Invocations list and metrics, replacing the duplicated inline diff.
- Fold the variables and root_run_id columns into their create migrations
  (this branch isn't in production) and drop the standalone alters.
- Import Illuminate\Http\Response (aliased) instead of referencing it inline.
2026-06-13 16:30:32 -03:00
Paulo Castellano
2bd2e72656 Validate webhook payload template is JSON before it can run
A webhook node parses its payload template as JSON before resolving
placeholders, so a template with unquoted {{ }} placeholders or any malformed
JSON could be saved, tested, and activated — only to fail midway through a run.

Reject it up front instead: AutomationConfigValidator is the single source of
truth for per-node config issues (keyed to the field the editor surfaces them
under), enforced on save (field errors), on activate, and before a test run.
The editor mirrors the check to disable Test/Activate with a clear reason, and
the test panel now surfaces the server's message instead of a generic toast.
2026-06-13 16:03:19 -03:00
Paulo Castellano
867c8d8acf Merge schedule summary and timezone into one line in trigger config
Show the user's timezone inline next to clock-pinned schedules
(Runs every day at 09:00 (GMT-3)) instead of on a separate hint line, and
only when the schedule pins a time of day — minute/hour intervals omit it.
2026-06-13 15:26:54 -03:00
Paulo Castellano
a129b6b5cd Denormalize automation trigger_type into an indexed column
The scheduler command ran every minute and loaded all active automations,
then filtered by trigger_type in PHP because that value lived buried in the
nodes JSON array — effectively a full-table scan plus a JSON decode per row
each minute, discarding every non-schedule automation.

Derive trigger_type into a real, indexed column on save (recomputed in the
existing saving() hook so it can never drift from nodes) and filter on it in
SQL. Applies to both the schedule firer and the post-trigger dispatcher.
2026-06-13 15:26:54 -03:00
Paulo Castellano
605261e1b8 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 15:04:24 -03:00
Paulo Castellano
ed5a757c4f Drop no-op log in Generate node
The no-active-accounts branch only logged and took no action; the
platforms loop already handles an empty active-account set.
2026-06-13 14:03:59 -03:00
Paulo Castellano
e77ba84f5d Drop redundant malformed-feed log in Fetch RSS node
The failed() result already surfaces the error; the Log::warning was
duplicate noise. Removes the now-unused Log import.
2026-06-13 14:02:42 -03:00
Paulo Castellano
478cad9f27 Tighten automation conventions: pagination, imports, i18n, enums
- Pagination: drop the perPage override from ListAutomations and the
  hardcoded page size from GetAutomationInvocations; both use
  config('app.pagination.default'). Document the rule in CLAUDE.md.
- Imports: import DomainException / InvalidArgumentException / Throwable
  instead of inline backslash references across the automation actions.
- i18n: move the hardcoded Fetch RSS and HTTP Request failure strings to
  automations.errors.* in all three locales.
- Publish: require scheduled_offset when mode is scheduled (validation)
  and drop the magic 60-minute default in the node. Fixes the required_if
  rules to reference the concrete node index instead of a wildcard that
  never resolved (also repairs the trigger cron rule).
- Condition handles: back the yes/no output handles with a shared
  Condition\Handle enum (PHP) and ConditionHandle const (TS).
2026-06-13 13:59:52 -03:00
Paulo Castellano
1b2ff23dff Guard expression resolver against malformed-UTF-8 json_encode
resolveVariable() declares a string return type, but json_encode returns
false on malformed UTF-8 (plausible for scraped feed/HTTP payloads),
which throws a TypeError under strict_types and fails the node. Encode
with JSON_PARTIAL_OUTPUT_ON_ERROR and fall back to an empty string.
2026-06-13 10:25:25 -03:00
Paulo Castellano
fe8cd8a970 Fix no-unused-expressions lint error in trigger weekday toggle
Replace the ternary-as-statement with an explicit if/else so
@typescript-eslint/no-unused-expressions stops flagging it.
2026-06-13 10:10:34 -03:00
Paulo Castellano
37a7a64ff0 Drop the custom-cron schedule option from automations
Remove the "Custom (Cron)" schedule field — too technical for the editor.
The remaining presets (minutes/hours/days/weeks/months) cover the need and
still build the cron string under the hood.

Removed at the root: the ScheduleField.Custom enum case, the custom-cron
input and select option, the schedule_custom_cron type field and its
schedule-summary handling, the backend validation (Rule::in and the
schedule_custom_cron rule), the i18n keys, and the custom round-trip test
case. Existing automations keep firing — the scheduler runs off the stored
cron string, not schedule_field.
2026-06-13 10:06:38 -03:00
Paulo Castellano
31b6544c57 Cover node-run error paths and align webhook failure handling
Wrap the webhook HTTP send in a try/catch so a connection error returns
a clean failed result (reason: request_failed) instead of bubbling up as
a job failure — matching the HTTP request node.

Add tests for the gaps in node-run coverage:
- HTTP request: basic auth, PUT/PATCH/DELETE, non-2xx responses,
  connection exceptions, and an items_path that doesn't resolve to a list.
- Webhook: every HTTP method, header expression resolution, and the new
  connection-failure path.
- Fetch RSS: non-2xx feed responses, malformed XML, items without a
  publish date (skipped), and the link fallback when an item has no guid.
- Delay: unknown unit throws.
- Publish: dry runs don't publish or queue.
2026-06-13 09:56:18 -03:00
Paulo Castellano
cda197adf8 Update automation guides and UI components
- Updated the 'guide' label in English, Spanish, and Portuguese translations to provide clearer context: changed from 'Guide' to 'Learn how it works' and its translations.
- Removed the EditorGuide component from the automation form, replacing it with a tooltip that links to the documentation.
- Adjusted the icon used for the help button from IconHelp to IconLifebuoy for better visual representation.
- Enhanced the tooltip functionality to provide guidance on automation actions directly within the UI.
2026-06-12 19:41:25 -03:00
Paulo Castellano
3e43da29e0 Add Workflow/Invocations/Metrics/Settings tabs to automations
Split the automation detail screen into four route-based tabs behind a
shared AutomationHeader:

- Workflow: the existing editor canvas.
- Invocations: a paginated, filterable run log with expandable per-node
  detail, a refresh control, and a loading state.
- Metrics: KPI cards, a runs-over-time @unovis chart with locale-aware
  date labels, and a posts-by-platform breakdown over a date range.
- Settings: rename, an activate/pause switch, and a danger-zone delete.

Invocations and Metrics report only real executions via a new
productionRuns scope, so manual test runs (dry or with real data) never
leak into the log or the charts. The now-unused excludingDryRuns scope
is removed.

Generated copy now flows the most-restrictive platform context through
the humanizer too, and the editor guide documents every available
expression grouped by source node.
2026-06-12 19:19:46 -03:00
Paulo Castellano
a09b1f45c2 Structure brand voice and make generated copy platform-aware
Replace free-text brand_tone/brand_voice_notes with a single structured
brand_voice_traits JSON column backed by the BrandVoiceTrait enum, exposed
as choice-chip pills in the brand settings UI and autofillable from a site.
Brand voice and visuals become per-automation toggles on the Generate node.

Unify the image controls into one 0-10 picker (0 = text-only, 1 = single,
2+ = carousel) and feed the generator the most restrictive selected network
so copy fits every platform. Pass that same platform context through the
humanizer pass — extracted into a shared ResolvesPlatformCopyBudget trait —
so the rewrite can no longer drift past the character cap the generator
respected, in both the automation and manual creation flows.

Persist the trigger node's schedule editor fields on save (they were
silently dropped by validated() for lacking validation rules).
2026-06-12 17:09:39 -03:00
Paulo Castellano
448ae73389 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 11:31:58 -03:00
Paulo Castellano
9a692b4608 Enhance automation functionality: Introduce workflow variables and improve node validation
- Added support for workflow variables in automations, allowing users to define reusable values.
- Implemented validation for Generate nodes to ensure intended image counts align with selected accounts.
- Updated automation models and requests to handle new variables, including encryption for sensitive data.
- Enhanced UI to display variables and their management within the automation editor.
- Improved error handling for webhook and HTTP nodes to prevent requests to invalid URLs.
- Refactored various components for better context resolution during automation runs.
2026-06-11 15:47:29 -03:00
Patrick Schrall
b317b8dbb2 Address Copilot review: normalize endpoints, quieter logs, robust tests
- resolveHandleToDid: rtrim trailing slash on the service URL before building
  the request URL and before the `=== $service` auth check (avoids double-slash
  URLs and mis-detected auth).
- Log per-endpoint resolution attempts at debug; emit a single warning only
  after all endpoints are exhausted (less noise during transient outages).
- Tests: fake resolveHandle with a wildcard so the case is isolated from the
  configured service URL; read request payload via $request->data().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:37:16 +02:00