Commit graph

14 commits

Author SHA1 Message Date
Paulo Castellano
35e7b703b6 fix(x): harden chunked media upload handling and coverage
Fail hard when media processing fails, map media-specific X invalid-request
errors clearly, and cover GIF/large-image/amplify/finalize/append failure paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 10:30:34 -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
2585d89cb4 Address PR review: connect-status enum, publisher cleanup, fill test gaps 2026-06-13 22:38:23 -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
123996bbc0 refactor(social): TokenRedactor::redact accepts nullable
Lets the call sites drop the explicit null check ternary. The redactor
itself owns the null handling — one less conditional at every caller.
2026-05-19 09:25:01 -03:00
Paulo Castellano
cf80e1fcae refactor(social): single source of truth for token redaction
Same shape of bug as the refreshToken duplication: the regex that strips
access_token / Bearer headers from logged HTTP bodies existed in three
near-identical copies (HasSocialHttpClient trait, TokenRefreshClient,
SocialPublishException), drifting subtly — SocialPublishException was
missing the JSON "token" pattern.

Extracts a TokenRedactor::redact(string) helper and routes all callers
through it. Adding a new token format now means one regex in one file.
2026-05-19 09:21:43 -03:00
Paulo Castellano
7d82cc3f12 fix(x): drop chunked upload chunk size from 5MB to 1MB
Production was returning HTTP 413 with empty body on the first APPEND
segment of every video upload to X v2 — surfacing to users as 'An
unknown X error occurred.'

Empty-body 413 is the classic signature of an edge/CDN rejection: the
X gateway is denying the request before X's application code sees it.
The X v2 reference docs say 'max chunk size: 5MB', but every canonical
reference uses 1MB:
- X's official Python quickstart: `chunk_size = 1024 * 1024`
- X's official JavaScript quickstart: `const chunkSize = 1024 * 1024`
- twitter-api-v2 (the most-used Node SDK, used by Postiz et al.):
  `chunkSize: number = 1024 * 1024`

5MB plus multipart-form overhead apparently exceeds an undocumented
edge limit. 1MB is the empirically safe size everyone converges on.

Changes:
- XPublisher chunked APPEND now uses 1MB chunks. An 8MB video uploads
  as 8 segments instead of 2; more roundtrips but actually succeeds.
- Set explicit Content-Type on each chunk attach (matches the simple
  upload path in the same file).
- XPublishException::fromApiResponse maps HTTP 413 to
  ErrorCategory::MediaFormat with the message 'Media chunk rejected
  by X (payload too large).' so we don't surface 413 as 'unknown' if
  it ever recurs.

Test: unit test covering the 413→MediaFormat mapping. Full suite green
(1503 passed, 2 skipped).
2026-05-12 20:02:23 -03:00
Paulo Castellano
da2b7ca9ca fix: Facebook/Instagram/Threads OAuthException type incorrectly treated as token error — now only code 190 triggers TokenExpiredException 2026-04-01 14:33:26 -03:00
Paulo Castellano
6e3dac7a66 fix: GD memory safety check and redact tokens from exception context 2026-03-31 22:01:59 -03:00
Paulo Castellano
30bfb2a890 refactor: use data_get in MediaOptimizer for consistency 2026-03-31 21:25:37 -03:00
Paulo Castellano
92ef7428b1 feat: add LinkedIn, X, Threads publish exceptions 2026-03-31 20:43:21 -03:00
Paulo Castellano
9999335da6 feat: add Pinterest, Bluesky, Mastodon publish exceptions 2026-03-31 20:43:13 -03:00
Paulo Castellano
6587c76bc2 feat: add InstagramPublishException with 25 error codes 2026-03-31 20:37:52 -03:00
Paulo Castellano
c3c51d4ccd feat: add ErrorCategory enum and SocialPublishException base class 2026-03-31 20:34:41 -03:00