Commit graph

621 commits

Author SHA1 Message Date
Falconiere Barbosa
a2a7f44c9a
refactor(linkedin): drive OAuth scopes from LINKEDIN_SCOPES env via config
Replace the additive LINKEDIN_EXTRA_SCOPES approach with a single
full-override env var per flow, exploded into an array at the config
layer (config/trypost.php -> platforms.linkedin{,-page}.scopes). This
keeps env values as plain comma-separated strings, lets self-hosters
override the entire set in one place, and removes the controller-side
scope-merge logic.

- config/trypost.php: explode LINKEDIN_SCOPES / LINKEDIN_PAGE_SCOPES
  into the scopes arrays (deprecated r_basicprofile stays out of the
  personal default)
- LinkedInController: drop resolveScopes(), read config scopes directly
- LinkedInPageController: drop the hardcoded $scopes property, read
  config scopes at both call sites
- tests: drive the connect scope assertions from config overrides
- .env.example, docker/.env.docker.example: document LINKEDIN_SCOPES
  and LINKEDIN_PAGE_SCOPES
2026-06-10 15:04:57 -03:00
Falconiere Barbosa
8e164bda36
refactor(linkedin): dedupe connect-scope test setup into shared closure 2026-06-10 15:04:57 -03:00
Falconiere Barbosa
21bbe96d6b
refactor(linkedin): move OAuth scopes into config/trypost.php platforms
Consolidate the default scope set alongside the existing LinkedIn host
config under config/trypost.php -> platforms.linkedin, matching the
project convention that per-platform service config lives there. The
default still drops the deprecated r_basicprofile scope, and
LINKEDIN_EXTRA_SCOPES stays additive (merged onto the defaults rather
than replacing them) so operators can opt back into legacy scopes
without risking a misconfigured full-replacement.

- config/trypost.php: add scopes + extra_scopes to platforms.linkedin
- config/services.php: drop the moved extra_scopes key
- LinkedInController::resolveScopes(): read both from trypost config
- tests: repoint config() overrides to the new key
2026-06-10 15:04:57 -03:00
Paulo Castellano
4987a31e8a
Merge branch 'main' into fix/linkedin-drop-r_basicprofile 2026-06-10 14:57:40 -03:00
Paulo Castellano
693e9c9b40
Merge pull request #82 from trypostit/69-feature-aspect-ratio-selection-for-facebook-posts-like-instagram
Aspect ratio selection for Facebook posts (like Instagram)
2026-06-10 14:56:14 -03:00
Paulo Castellano
03e129f401
Merge branch 'main' into 69-feature-aspect-ratio-selection-for-facebook-posts-like-instagram 2026-06-10 14:37:30 -03:00
Paulo Castellano
03a3d9eb7e
Merge branch 'main' into fix/linkedin-drop-r_basicprofile 2026-06-10 14:31:56 -03:00
Paulo Castellano
2b620124d2
Merge pull request #85 from trypostit/chore/copilot-review-instructions
Add Copilot code review instructions
2026-06-10 14:31:44 -03:00
Paulo Castellano
bcefe66da2 Enrich Copilot review instructions with examples and missing conventions 2026-06-10 14:26:54 -03:00
Paulo Castellano
9c15e43e91 Add Copilot code review instructions 2026-06-10 14:22:16 -03:00
Paulo Castellano
3fe70b1402 refactor(PostEditor): update PostEditorTabs integration and enhance schedule tab visibility logic 2026-06-10 13:58:45 -03:00
Paulo Castellano
159e1a34f3 feat(PostEditorTabs): add a new component for managing post editing tabs including preview, schedule, and comments functionalities 2026-06-10 13:58:20 -03:00
Falconiere Barbosa
410eb9612e fix(linkedin): drop deprecated r_basicprofile from default scopes
Make r_basicprofile opt-in via LINKEDIN_EXTRA_SCOPES so self-hosted users
unblock by default and ops with legacy/enterprise products keep working.

Why
---
LinkedIn rejects OAuth authorize requests with a generic "Bummer,
something went wrong" page when an app asks for a scope it can't grant.
`r_basicprofile` is a legacy scope deprecated in 2018; new LinkedIn dev
apps don't have it, so every self-hosted user hits the rejection
immediately on `/connect/linkedin`.

The two products LinkedIn actually grants to standard apps today are:

- Sign In with LinkedIn using OpenID Connect → `openid profile email`
- Share on LinkedIn                          → `w_member_social`

That set is enough for the connect flow. The only piece of data
`r_basicprofile` was buying us is `/v2/me`'s `vanityName` (pretty
`linkedin.com/in/<slug>`). `fetchVanityName()` already handles HTTP
failure gracefully (returns null), and the only downstream consumer —
`LinkedInPagePublisher`'s post-URL builder — already falls back to a
numeric `linkedin.com/feed/update/<id>` URL when `$account->username`
is null.

Backward compatibility
----------------------
Ops with legacy or enterprise LinkedIn products approved on their dev
app (so they DO have `r_basicprofile`) can opt back in via env:

    LINKEDIN_EXTRA_SCOPES=r_basicprofile

`LinkedInController::resolveScopes()` merges this comma-separated list
into the default scope array. The connect flow's `Socialite::scopes()`
call then includes the legacy scope, preserving the pre-PR behaviour
end-to-end (including `vanityName` lookup).

Net effect for users without `r_basicprofile`:
- Connect flow works (was previously rejected by LinkedIn).
- Posts publish exactly the same way.
- Generated post URLs use the numeric form instead of the vanity slug.

Tests
-----
- `linkedin connect requests the default scope set when LINKEDIN_EXTRA_SCOPES is unset`
- `linkedin connect appends LINKEDIN_EXTRA_SCOPES to the default scope set`
- Existing `splits comma-separated approvedScopes` fixture updated to
  match the new default set.
2026-06-10 13:51:07 -03:00
Paulo Castellano
82eeaa82d9
Merge branch 'main' into 69-feature-aspect-ratio-selection-for-facebook-posts-like-instagram 2026-06-10 13:14:55 -03:00
Paulo Castellano
7c0c38698a
Merge pull request #71 from trypostit/fix/tiktok-app-review
fix(tiktok): align UX strings and render processing notice for app review
2026-06-10 13:12:55 -03:00
Paulo Castellano
d3e9be54b6 refactor(seeder): remove conditional UserSeeder call from DatabaseSeeder
Removed the logic that conditionally seeds the UserSeeder based on the self-hosting configuration. This simplifies the DatabaseSeeder and assumes user data will be handled externally.
2026-06-10 13:08:08 -03:00
Paulo Castellano
cbb73487f2
Merge branch 'main' into fix/tiktok-app-review 2026-06-10 13:02:25 -03:00
Paulo Castellano
bc44388570 feat(seeder): conditionally seed UserSeeder for self-hosted setups
Added logic to the DatabaseSeeder to call UserSeeder only if the application is configured for self-hosting. This ensures that user data is seeded appropriately based on the deployment environment.
2026-06-10 13:01:32 -03:00
Paulo Castellano
17e6da7ae3
Merge branch 'main' into 69-feature-aspect-ratio-selection-for-facebook-posts-like-instagram 2026-06-05 19:56:37 -03:00
Paulo Castellano
8eb175a6a5
Merge pull request #76 from sebastian-works/main
fix: Updates symfony packages from 8.0.x to 8.1.0 to address the following security advisories:
2026-06-05 19:31:20 -03:00
Paulo Castellano
b36f069e9a
Merge branch 'main' into main 2026-06-05 19:28:28 -03:00
Paulo Castellano
5be0abfae3 Cover carousel/multi crop per-ratio and FB multi abort-on-crop-failure
- Instagram carousel and Facebook multi-image crop tests now run as datasets
  over 1:1 and 4:5, asserting each image's real cropped ratio (previously 1:1
  only) — proving the chosen ratio flows through the multi-image paths.
- Add a Facebook test that a multi-image post aborts entirely (throws
  FacebookPublishException) when one image can't be downloaded for cropping.
2026-06-05 18:45:48 -03:00
Paulo Castellano
1b096f9ede Unify crop storage folder and make IG/FB crop tests symmetric
- CropsImageForAspectRatio: drop the per-publisher path-prefix argument; crops
  from both platforms go to a single `social-crops/` directory (the folder has
  no functional meaning — the platform fetches by URL and nothing reads it back;
  UUID filenames prevent collisions).
- Close the IG/FB test asymmetry surfaced in review: Instagram single-image crop
  now runs across 1:1/4:5/16:9 (dataset) and has a crop-download-failure test;
  Facebook gains an explicit 'original' bypass test. Both platforms now cover the
  same matrix (crop per ratio + uploaded-URL is the crop, original/no-meta
  bypass, multi-image, download-failure exception).

Follow-up tracked in #83 (orphaned crops are never pruned from storage).
2026-06-05 18:30:17 -03:00
Paulo Castellano
e25b320ad2 Add aspect ratio selection for Facebook posts
Facebook feed posts now have the same aspect ratio selector as Instagram
(1:1, 4:5, 16:9, Original), letting users control how their image is cropped
before publishing instead of leaving it to Facebook's auto-crop.

The Facebook Graph API has no crop/aspect-ratio parameter for organic Page
photo posts (image crops are ads-only) — Facebook displays the bytes it
receives at their native ratio. So, exactly like Instagram, the crop is done
server-side before upload.

- Extract the Instagram crop logic into a shared CropsImageForAspectRatio
  concern (download -> crop -> store -> URL), with a per-publisher
  cropFailureException hook. InstagramPublisher refactored to use it
  (behaviour identical; existing tests unchanged).
- FacebookPublisher reads meta.aspect_ratio and crops single- and multi-image
  feed posts (facebook-crops/ prefix). Reels/Stories are unaffected.
- FacebookSettings: aspect ratio picker on the Feed variant (default Original
  -> no crop). ScheduleTab wires meta; FacebookPreview reflects the chosen
  ratio. Adds posts.form.facebook.aspect.* in en/es/pt-BR.

Closes #69
2026-06-05 18:01:45 -03:00
Paulo Castellano
a06fe78d62
Merge pull request #80 from trypostit/fix/instagram-carousel-content-type
Stop persisting instagram_carousel as a content type
2026-06-04 20:10:59 -03:00
Paulo Castellano
cca7893e0f Stop persisting instagram_carousel as a content type
Instagram carousels were stored as content_type=instagram_carousel, which the
publisher's match() did not handle — publishing failed with "Unsupported
Instagram content type: instagram_carousel" for any post created via API, MCP,
or template (the AI flow worked only thanks to an inline band-aid that rewrote
carousel to feed before saving).

A carousel is just an Instagram feed post with multiple images: the editor,
preview, and publisher already treat a multi-image feed as a carousel. So
instagram_carousel is a generation format, not a stored content type. Remove it
from the ContentType enum entirely; it now lives only as an AI generation-format
string (wizard card + slide structure + carousel templates are untouched), and
posts always persist as instagram_feed.

- ContentType: drop the InstagramCarousel case; InstagramFeed maxMediaCount 1 -> 10
- StreamPostCreation: resolvedContentType() maps carousel -> feed; band-aid removed
- StartPostCreationRequest: accept instagram_carousel as a generation format
- Frontend: carousel becomes a wizard-local AiFormat; UI/UX unchanged
- API/MCP now reject instagram_carousel as a content_type (Rule::in no longer lists it)
2026-06-04 18:10:39 -03:00
Sebastian Albert
454aa8ad71
Merge branch 'trypostit:main' into main 2026-06-03 00:14:44 +02:00
Paulo Castellano
afae1a57c0
Merge pull request #78 from trypostit/fix/calendar-shows-no-content
Fix calendar showing "no content" for every post
2026-06-02 09:19:21 -03:00
Paulo Castellano
ba98f01530 Fix calendar showing 'no content' for every post
The calendar read each post's caption from post_platforms[0].content,
but post_platforms has no content column — the caption lives on
posts.content. As a result every cell fell back to the 'no content'
placeholder. Read post.content instead (matching the list view) and
drop the phantom content field from the PostPlatform interface.

Also surface the caption in the month view (it previously showed only
the time and platform icons), add horizontal padding to the month post
list so the card border is not clipped inside the overflow-y scroll
container, and standardise the hover effect across day/week/month
views to a shadow-only highlight (no lift).

Closes #70
2026-06-02 09:16:16 -03:00
Paulo Castellano
7b4553fdbb
Merge pull request #77 from trypostit/fix/workspace-name-only-update
Fix workspace name-only update rejected by required brand fields
2026-06-02 08:49:18 -03:00
Paulo Castellano
2f047f2f7e Fix workspace name-only update rejected by required brand fields
The Workspace and Brand settings tabs both submit to the same
updateSettings endpoint, but the Workspace tab sends only the name.
UpdateWorkspaceRequest marked brand_font and image_style as required,
so the name-only request failed validation silently (the form only
renders the name error). Mark both brand fields as sometimes|required
so a name-only update succeeds while the Brand tab still validates them.

Closes #74
2026-06-02 08:45:11 -03:00
sebastian-works
33ccd0a0df fix: update symfony packages to 8.1.0 for security advisories (CVE-2026-45067 et al.) 2026-05-31 00:43:01 +02:00
Paulo Castellano
c152db5431
Merge pull request #75 from trypostit/update-readme-stars
Update star history section for dark and light themes
2026-05-29 16:18:06 -03:00
Paulo Castellano
4d39927472
Update star history section for dark and light themes 2026-05-29 16:11:24 -03:00
Paulo Castellano
65afd86824 fix(tiktok): default disable_comment to true when meta omits allow_comments
The Vue computed property returns false for allow_comments when meta lacks
the key (so the UI checkbox renders unchecked), but the backend defaulted
allow_comments to true if the key was absent — sending disable_comment=false
to TikTok. A post created without the user toggling the comment checkbox
went out with comments enabled even though the UI showed them off.

Align the backend default with what the UI displays: when meta omits
allow_comments, treat it as off (matching duet and stitch, which already
default to off). Adjust the existing default-settings test that locked in
the old behaviour.
2026-05-29 10:30:06 -03:00
Paulo Castellano
50477b97f0 fix(tiktok): align UX strings and render processing notice for app review
Match the literal copy the TikTok app reviewer looks for during the
Content Posting API audit, per the Mixpost playbook (the only public
account of how to pass).

- Privacy dropdown placeholder: "Select who can view this post"
- Tooltip on interaction toggles disabled by creator: "Turned off in
  your TikTok account settings"
- Render the "may take a few minutes" notice that was translated but
  never displayed (processing_hint)

Mirror both i18n changes in pt-BR and es. Add a feature test that locks
the literal strings so they cannot drift before the next review cycle.
2026-05-29 10:16:04 -03:00
Paulo Castellano
ec7aad7044
Merge pull request #67 from trypostit/paulocastellano-patch-1
Update GitHub funding username
2026-05-27 18:01:08 -03:00
Paulo Castellano
37d8699b1f
Update GitHub funding username 2026-05-27 17:54:16 -03:00
Paulo Castellano
5de993bc6c
Merge pull request #65 from trypostit/feat/docker-distribution
feat(docker): publish multi-arch image to GHCR + self-host compose
2026-05-27 16:02:32 -03:00
Paulo Castellano
09fa97d539 refactor(docker): align echo.ts with the shared sendkit config
Make resources/js/echo.ts byte-identical to the sendkit Echo setup
(VITE_REVERB_* read directly, no window.location derivation). The release
workflow now bakes only VITE_REVERB_APP_KEY, and compose.prod.yaml restores
the 8080 host port so the published image's Reverb client (baked at
localhost:8080) works for local docker compose. A custom-domain deploy needs
the image rebuilt with VITE_REVERB_HOST/PORT/SCHEME set.
2026-05-27 15:17:47 -03:00
Paulo Castellano
48727c80cd feat(docker): add Caddy reverse proxy and domain-portable Reverb config
Add an optional Caddy service (proxy profile) to compose.prod.yaml for
automatic HTTPS on a custom domain, with a Caddyfile that transparently
proxies WebSocket upgrades.

Move Echo setup into resources/js/echo.ts and derive the Reverb host from
window.location when VITE_REVERB_* is unset, so the published image's frontend
connects on any domain without a rebuild. The release workflow bakes a fixed
public Reverb key and leaves host/port/scheme empty to enable this; the Herd
dev setup keeps its explicit VITE_REVERB_* values, so its behaviour is
unchanged.

Drop the now-unneeded 8080 host port — the WebSocket rides the main HTTP(S)
port via the in-container nginx /app proxy.
2026-05-27 14:53:46 -03:00
Paulo Castellano
5b9226569f feat(docker): publish multi-arch image to GHCR and add self-host compose
Add a release workflow that builds docker/Dockerfile (production target) for
linux/amd64 and linux/arm64 on native runners and publishes a single
multi-arch manifest to ghcr.io/trypostit/trypost on every v*.*.* tag, tagged
with semver (1.2.3, 1.2, 1) and latest.

Add compose.prod.yaml: a self-host stack that pulls the published image
alongside Postgres and Redis with persistent volumes. Config is inline with
sectioned comments, including a commented S3/R2 block (storage already uses
the default disk, so switching is config-only).

No application code is changed.
2026-05-27 14:40:35 -03:00
Paulo Castellano
0602a02706
Merge pull request #63 from trypostit/docs/readme-markdown
docs: add Star History section to README
2026-05-25 17:11:43 -03:00
Paulo Castellano
7577029045 docs: add Star History section to README 2026-05-25 17:08:01 -03:00
Paulo Castellano
fc18f044af chore(release): enhance /release command for improved automation
Refines the /release slash command to streamline the weekly release process, ensuring better detection of version bumps and integration with the GitHub release API for automated note generation and email drafting.
2026-05-22 13:39:05 -03:00
Paulo Castellano
2cbf1d5be1
Merge pull request #62 from trypostit/chore/add-release-command
chore(release): add /release slash command for weekly ritual
2026-05-22 13:17:05 -03:00
Paulo Castellano
ff7ca65dc2 chore(release): switch to sequential versioning with rollover at 9
Replaces semver-by-commit-type detection with sequential numbering.
Patch increments on every release; rolls over to minor at patch=10;
rolls over to major at minor=10. No manual override — every release
is the next number in sequence.
2026-05-22 13:07:01 -03:00
Paulo Castellano
7e4fab3078 chore(release): add /release slash command for weekly ritual
Adds a Claude Code slash command that runs the Friday release flow:
auto-detects semver bump, generates the GitHub release via the native
generate-notes API, drafts a customer-facing email in Cal.com style
(themed prose, no internal references), runs the humanizer skill on the
email prose, and saves mirrors to releases/<version>/.
2026-05-22 13:00:50 -03:00
Paulo Castellano
d422289808
Merge pull request #60 from trypostit/fix/facebook-story-video-only
fix(facebook): restrict Page Stories to video-only
2026-05-21 21:25:03 -03:00
Paulo Castellano
8dfa664fa6 fix(facebook): restrict Page Stories to video-only
Facebook Stories do not work reliably with images via the Graph API.
Align validation and publisher with video-only (like Reel), while
Instagram Story continues to accept image or video.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 21:21:50 -03:00