trypost/app/Services/Social/Concerns/HasSocialHttpClient.php
Paulo Castellano 953be22b5b fix(posts): block scheduling when content exceeds any platform's char limit
Threads posts over 500 chars were saved + scheduled successfully and only
failed inside the publish job. The frontend already showed the 537|500 badge
but `canSchedule` ignored content length, so Schedule and Post Now stayed
enabled. Backend `UpdatePostRequest` only capped at 63206 (Facebook's max),
not per-platform.

- Add `Platform::contentOverflow()` as the single source of truth and reuse it
  from `HasSocialHttpClient::validateContentLength` (publish-time).
- New `ContentFitsPlatformLimits` rule applied to the `content` field on
  `App\\UpdatePostRequest`, `Api\\UpdatePostRequest`, and `Api\\StorePostRequest`
  via `Rule::when(...)` so drafts are not blocked.
- Rule dedupes per platform (two Threads accounts -> one error) and reports
  the platform label, hard cap, and overage via i18n.
- Edit.vue feeds `contentLengthOverflows` into `canSchedule` and lists each
  offending platform in `postActionTooltip` using the existing
  `getPlatformLabel` resolver.
2026-05-11 19:39:41 -03:00

78 lines
2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Social\Concerns;
use App\Models\PostPlatform;
use App\Models\SocialAccount;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
trait HasSocialHttpClient
{
protected function validateContentLength(PostPlatform $postPlatform): void
{
$content = $postPlatform->post->content ?? '';
if ($postPlatform->platform->contentOverflow($content) === null) {
return;
}
$maxLength = $postPlatform->platform->maxContentLength();
$contentLength = mb_strlen($content);
throw new \Exception(
"Content exceeds {$postPlatform->platform->label()} limit of {$maxLength} characters ({$contentLength} provided)."
);
}
protected function refreshTokenWithLock(SocialAccount $account, callable $refreshFn): void
{
$lock = Cache::lock("token_refresh:{$account->id}", 30);
if (! $lock->get()) {
// Another process is refreshing, wait and reload
sleep(2);
$account->refresh();
return;
}
try {
$refreshFn();
} finally {
$lock->release();
}
}
protected function socialHttp(): PendingRequest
{
return Http::retry(
times: 3,
sleepMilliseconds: 5000,
when: fn ($exception, $request) => $exception->response?->status() === 429,
throw: false,
)->timeout(120);
}
protected function redactResponseBody(string $body): string
{
return preg_replace(
[
'/access_token=([^&"\s]+)/',
'/"access_token"\s*:\s*"([^"]+)"/',
'/Bearer\s+\S+/',
'/"token"\s*:\s*"([^"]+)"/',
],
[
'access_token=[REDACTED]',
'"access_token":"[REDACTED]"',
'Bearer [REDACTED]',
'"token":"[REDACTED]"',
],
$body
);
}
}