Commit graph

773 commits

Author SHA1 Message Date
Paulo Castellano
8d608554cd feat(ai-templates): tweet-card-with-photo thumbnail 2026-06-17 19:53:38 -03:00
Paulo Castellano
0574560570 feat(ai-templates): ContentStyle enum as the typed source for the 3 styles 2026-06-17 19:52:38 -03:00
Paulo Castellano
d8291cb155 feat(ai-templates): tweet-card-with-photo style (blurred dark image background) 2026-06-17 19:43:21 -03:00
Paulo Castellano
b10cef7a9b feat(ai-templates): landscape image-card thumbnail showing the headline (fits the picker frame) 2026-06-17 19:26:36 -03:00
Paulo Castellano
9f7b797674 feat(tweet-card): tabler verified badge (svg source committed) + badge placement next to name 2026-06-17 19:21:19 -03:00
Paulo Castellano
8317b83d6b fix(ai-templates): skip humanizer for tweet_card style regardless of carousel mode
The humanize() guard was keyed on format string 'tweet_card', but for tweet_card
carousel the format becomes 'carousel'. Changed the guard to key on the style key
($this->template === 'tweet_card') so humanization is skipped for all tweet_card
variants — single and carousel alike.
2026-06-17 19:01:10 -03:00
Paulo Castellano
58ac2714d0 feat(ai-templates): styles orthogonal to single/carousel; tweet-card carousel
- TemplateContext gains bool $isCarousel (set from format === CAROUSEL_FORMAT in StreamPostCreation)
- AiContentTemplate interface: promptView() now takes TemplateContext
- ImageCardTemplate handles both single and carousel via context.isCarousel; schema() returns the correct shape per mode; assemble() routes to assembleSingle/assembleCarousel
- TweetCardTemplate handles both single and carousel; promptView() returns tweet_card_carousel view when isCarousel; schema() returns {tweet_text} or {caption, slides[]}; assemble() calls forTweetCard (single) or forTweetCardCarousel (carousel)
- CarouselTemplate deleted — carousel is now a format dimension each style handles, not a standalone style
- AiTemplateRegistry: [ImageCardTemplate, TweetCardTemplate] only
- PostImagePipeline: adds forTweetCardCarousel — renders one renderTweetCard per slide text, returns N media items
- StreamPostCreation: carousel detection restored to $format === CAROUSEL_FORMAT (like main); style dispatched via AiTemplateRegistry::find($this->template); humanize skips tweet_card (single and carousel)
- PostContentGenerator: promptView() call updated to pass templateContext
- New prompt: resources/views/prompts/post_content/tweet_card_carousel.blade.php
- Tests: TemplateContractTest and TweetCardTemplateTest updated for promptView(context) signature; AiTemplateRegistryTest asserts [image_card, tweet_card]; StreamPostCreationTest adds tweet_card carousel test; TweetCardTemplateTest covers both schema shapes and both promptView return values
2026-06-17 18:59:41 -03:00
Paulo Castellano
e886db5621 feat(ai-templates): choose visual style after the format, on the same screen 2026-06-17 18:45:03 -03:00
Paulo Castellano
5184ff3013 feat(ai-templates): real rendered thumbnails for the template picker 2026-06-17 18:34:24 -03:00
Paulo Castellano
bb55e14423 fix(ai-templates): paragraph spacing bug, registry cache, coverage gaps, empty-account UX 2026-06-17 18:09:02 -03:00
Paulo Castellano
24d9653f6e fix(ai-templates): real needsAccount + content_type test coverage; drop unwired humanizableFields; cleanup 2026-06-17 17:56:52 -03:00
Paulo Castellano
799a151321 feat(ai-templates): template picker in the AI wizard 2026-06-17 17:42:23 -03:00
Paulo Castellano
4656cb52c6 feat(ai-templates): i18n template names (en, es, pt-BR) 2026-06-17 17:37:51 -03:00
Paulo Castellano
9d8f772a3e feat(tweet-card): tweet_card template wired end-to-end 2026-06-17 17:35:18 -03:00
Paulo Castellano
0d4f3b90d7 feat(tweet-card): solid-background tweet card image layout 2026-06-17 17:32:10 -03:00
Paulo Castellano
8dfdb2427a feat(tweet-card): prompt + verified badge + thumbnail assets 2026-06-17 17:29:28 -03:00
Paulo Castellano
8c72719f92 feat(ai-templates): validate template key + require account when needed 2026-06-17 17:27:58 -03:00
Paulo Castellano
9dba90129f feat(ai-templates): task 3 - template-driven generator + job
- Add AiTemplateRegistry (resolves, keys, default)
- Add template/templateContext params to PostContentGenerator; schema() delegates to template when both are set; instructions() uses template.promptView()
- Refactor StreamPostCreation.handle() to resolve template via registry, build TemplateContext, delegate assemble() to template; add createPostFromGenerated()
- Remove handleSingle(), handleCarousel(), resolvedContentType() (no other callers)
- PostAiCreateController passes template param from request (default image_card)
- All existing AI tests green; registry test added
2026-06-17 17:23:33 -03:00
Paulo Castellano
97798c010a feat(ai-templates): extract image-card + carousel templates 2026-06-17 17:05:53 -03:00
Paulo Castellano
ebd9a2ac0f feat(ai-templates): content template contract + value objects 2026-06-17 17:03:42 -03:00
Paulo Castellano
34c49c5ce8
Merge pull request #102 from trypostit/chore/release-v1.0.3-artifacts
chore(release): add v1.0.3 changelog artifacts
2026-06-17 09:55:13 -03:00
Paulo Castellano
7dac201ab3 chore(release): add v1.0.3 customer email draft 2026-06-17 09:51:55 -03:00
Paulo Castellano
bdd5829ef3 chore(release): add v1.0.3 changelog artifacts 2026-06-17 09:50:38 -03:00
Paulo Castellano
c1b482acc3
Merge pull request #97 from sebastian-works/main
fix: resolve npm & composer security vulnerabilities
2026-06-16 19:00:50 -03:00
Paulo Castellano
5ccd00ffa2
Merge branch 'main' into main 2026-06-16 18:57:08 -03:00
Paulo Castellano
ce1073918a
Merge pull request #101 from trypostit/feat/mcp-api-platform-meta
Full per-platform meta parity for posts (API + MCP)
2026-06-16 18:14:46 -03:00
Paulo Castellano
40425d7bdc test,refactor: review fixes for per-platform meta parity
- Assert nested Discord meta keys (mention token/label, embed title/color)
  survive validated(), replacing a vacuous coalesced assertion.
- Cover the MCP publish guard for TikTok privacy and Pinterest board, not
  just Discord.
- Use assertUnprocessable() to match sibling API tests; type-hint the API
  UpdatePostRequest withValidator closure.
2026-06-16 18:11:29 -03:00
Paulo Castellano
bf399c80fa docs: document the single-source PostPlatform.meta validation rule
Record in CLAUDE.md that all platforms.*.meta validation and required-on-publish
logic lives only in PostPlatformMetaRules, why (validated() strips unlisted keys),
and where to add new per-platform meta + tests.
2026-06-16 17:52:50 -03:00
Paulo Castellano
ba20534a6d refactor(meta): own the platforms.*.meta parent rule in PostPlatformMetaRules
The shared rules() returned only meta sub-keys, so every caller still repeated
its own 'platforms.*.meta' => array parent (with slightly different sometimes/
nullable combos). Fold the parent into rules() with a single safe contract so
callers just spread one source for the whole meta block.
2026-06-16 17:50:40 -03:00
Paulo Castellano
3aa4b2a2f0 test(api,mcp): cover per-platform meta across networks
Broaden the post meta tests beyond Discord: store persists Instagram aspect
ratio, Pinterest board, TikTok privacy + flags; API update enforces TikTok
privacy and Pinterest board on publish and allows drafts without required meta;
MCP update merges meta and the publish guard ignores disabled platforms.
2026-06-16 17:44:27 -03:00
Paulo Castellano
bdea56d5e1 feat(api,mcp): full per-platform meta parity for posts
Centralize per-platform PostPlatform.meta validation in PostPlatformMetaRules
(shared by web, REST API and MCP). The API and MCP previously only accepted
aspect_ratio, silently stripping channel_id/board_id/privacy_level and the rest
via validated(), so Discord/Pinterest/TikTok couldn't be configured or published
through those entry points. Now all per-platform meta is accepted and persisted,
required-on-publish is enforced on API update (TikTok privacy, Pinterest board,
Discord channel), and the MCP publish tool guards a post's stored meta before
dispatching. Adds API + MCP tests and a PostPlatform discord() factory state.
2026-06-16 17:39:07 -03:00
Paulo Castellano
206d01f77e
Merge pull request #100 from trypostit/feat/discord-channel
feat(channels): add Discord as a social channel
2026-06-16 17:13:47 -03:00
Paulo Castellano
cc8f6625a1 feat(channels): Discord post-level engagement metrics
Add per-post Discord metrics (server member count, thread replies as comments,
and reaction counts) read live with the bot token via GET message + guild
with_counts, wired into PostMetricsFetcher. Redesign the shared post metrics
row: monochrome line icons for member/comment counts vs clean reaction pills,
so audience stats read distinctly from reactions. Applies to Telegram too.
2026-06-16 17:05:13 -03:00
Paulo Castellano
bf0494fbe6 fix(channels): restrict Discord picker to postable channels the bot can use
Only text (0) and announcement (5) channels accept a direct message — forum
(15) and media (16) require thread creation, so they're dropped from the
picker (they returned 'non-text channel'). Channels are also filtered by the
bot's effective VIEW_CHANNEL + SEND_MESSAGES, computed from role base perms and
channel overwrites (Discord's documented algorithm), with a safe fallback to
type-only filtering when the bot's roles can't be resolved.
2026-06-16 16:34:02 -03:00
Paulo Castellano
df81532c67 feat(channels): show channel + mentions in Discord preview, light theme
Persist channel_name (display-only) so the preview header shows the chosen
channel; render mentions as pills; restyle the preview to a light theme to
match the rest of the app.
2026-06-16 16:14:59 -03:00
Paulo Castellano
3547b471ea feat(discord): implement channel caching and enhance UI for channel selection
- Introduced caching for Discord channel lookups, reducing API calls and improving performance.
- Updated the DiscordSettings component to use a SearchableSelect for better user experience when selecting channels.
- Added new language strings for channel search and no channels found messages in English, Spanish, and Portuguese.
- Updated tests to ensure channel list caching is isolated between runs.
2026-06-16 15:53:20 -03:00
Paulo Castellano
47c3b4a9e9 chore(channels): document Discord env vars in .env.example 2026-06-16 15:28:58 -03:00
Paulo Castellano
5a54bf17fb fix(channels): discord review round 5 — asset, channel fallback, media test
- Add a placeholder discord.png so the brand mark isn't a broken image
  (operators can swap in the official logo).
- Keep a saved Discord channel selectable before the live channel list loads
  (or if the lookup is unavailable), so editing a post never loses the channel.
- Cover the media multipart upload path (payload_json + files[N]) with a test.
2026-06-16 15:04:46 -03:00
Paulo Castellano
c2dd4515b2 feat(channels): add Discord as a social channel
Connect a Discord server via OAuth (bot authorization) and schedule/publish
messages to its channels, with mentions and rich embeds.

- Connect: custom Socialite Discord provider (bot scope) maps the authorized
  guild to a SocialAccount; throws if no server was authorized.
- Publish: DiscordPublisher posts via the global bot token, validates the chosen
  channel belongs to the connected guild (anti cross-guild), optimizes media,
  builds allowed_mentions only from explicit mention chips (no accidental pings),
  and renders rich embeds.
- Compose: per-post channel picker (live lookup), mention autocomplete and an
  embed editor, gated by a required-channel compliance rule; Discord post preview.
- Enum/config/content-type wiring, ConnectionVerifier health check, throttled
  lookup endpoints, i18n (en/es/pt-BR), and tests.

Operators must create a Discord application and set DISCORD_CLIENT_ID,
DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN and DISCORD_CLIENT_REDIRECT.
2026-06-16 14:44:00 -03:00
Paulo Castellano
7d52ea72c5
Merge pull request #99 from trypostit/feat/rss-multiformat-simplepie
feat(automations): multi-format RSS/Atom feeds with dynamic variables
2026-06-16 12:24:24 -03:00
Paulo Castellano
45da024fc6 fix(automations): code-review hardening for feed fetch & URL validation
- inspectFeed fetches via SafeHttpFetcher::get (SSRF guard + timeout + redirect
  cap + UA) instead of a raw, timeout-less Http::get; add a timeout to the node fetch.
- ResolvableUrl now requires an http(s) scheme, so it's no weaker than the plain
  url rule it replaced (rejects file://, javascript://).
- Apply ResolvableUrl to the HTTP Request and Webhook node URLs too, so templated
  {{ }} URLs validate consistently across every URL field.
- Clear a fetch_rss node's discovered_fields when its feed_url changes, so stale
  fields from the previous feed stop showing up as autocomplete suggestions.
2026-06-16 12:09:16 -03:00
Paulo Castellano
f55d35363f fix(automations): return null on any feed parse failure
Wrap FeedParser::parse in try/catch so an unexpected Throwable from third-party
feed content (not just invalid XML) maps to the clean fetch_rss_malformed
failure instead of bubbling up as an unhandled exception — matching the
failed()-not-throw pattern of the sibling HTTP node.
2026-06-16 12:09:16 -03:00
Paulo Castellano
b2932262cf fix(automations): reliably open a node's config when switching nodes
A click with a few px of jitter crossed Vue Flow's 1px drag threshold and was
treated as a drag, so onNodeClick never fired and the config panel didn't switch
between nodes. Raise nodeDragThreshold to 5 so normal clicks stay clicks, and
also select the node on drag start as a safety net.
2026-06-16 11:26:05 -03:00
Paulo Castellano
109ad7dfc0 fix(automations): preview shows the newest feed item, not the oldest
A manual/dry "test with real data" run surfaces a single item and never fans
out, but it was taking the oldest new item (items are sorted oldest-first for
production fan-out). Surface the newest item in preview instead — what the user
expects to test against. Real runs are unchanged. Works for every feed format.
2026-06-16 11:26:05 -03:00
Paulo Castellano
4bb7d450a3 chore(release): link GitHub release in changelog email + add v1.0.2 artifacts
Tweak the /release command's email template (GitHub release link in the byline,
"Paulo from TryPost.it" signature) and add the generated v1.0.2 release notes.
2026-06-16 11:09:25 -03:00
Paulo Castellano
3acaafb81e test(automations): extract inline feed XML into shared fixtures
Move the RSS sample feeds out of inline heredocs/constants in FetchRssNodeTest
into tests/fixtures/feeds/, matching the parser fixtures. Centralize the
feedFixture() loader in tests/Pest.php and use it across the feed tests.
2026-06-16 11:06:46 -03:00
Paulo Castellano
246a159f34 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 10:56:08 -03:00
Sebastian Albert
fdb55e88a3
Rename project from 'post.fotohof.at' to 'trypost' 2026-06-15 21:59:00 +02:00
Sebastian Albert
8e2ae4d589
Merge branch 'trypostit:main' into main 2026-06-15 21:57:28 +02:00
Paulo Castellano
b18b9b2303
Merge pull request #98 from trypostit/feat/discord-widget
feat(discord): global floating Discord widget
2026-06-15 13:28:20 -03:00