Source path: app/Services/Publishers/XPublisher.php
Extracted from supplied specification. Review before copying into application.

<?php
namespace App\Services\Publishers;

use App\Models\Media;
use App\Models\PostTarget;
use App\Models\SocialAccount;
use GuzzleHttp\Psr7\Utils;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class XPublisher extends BasePublisher
{
    private const API = 'https://api.x.com/2';

    public function discoverAccounts(string $t): array
    {
        $u = Http::withToken($t)->get(self::API . '/users/me')->throw()->json('data');
        return [['id' => $u['id'], 'name' => '@' . $u['username']]];
    }

    public function refreshToken(SocialAccount $a): void
    {
        $c = config('social.x');
        $r = Http::asForm()->withBasicAuth($c['client_id'], $c['client_secret'])->post($c['token_url'], [
            'grant_type' => 'refresh_token', 'refresh_token' => $a->refresh_token, 'client_id' => $c['client_id'],
        ])->throw();
        $this->saveTokens($a, $r->json()); // X rotates refresh tokens
    }

    public function publish(PostTarget $t): array
    {
        $tok = $this->token($t->socialAccount);
        $media = $this->media($t);
        $video = $media->firstWhere('type', 'video');
        $items = $video ? collect([$video]) : $media->where('type', 'image')->take(4);

        $ids = $items->map(fn (Media $m) => $m->type === 'video' ? $this->uploadVideo($tok, $m) : $this->uploadImage($tok, $m))->all();

        $body = ['text' => Str::limit($t->post->caption, 280, '')];
        if ($ids) $body['media'] = ['media_ids' => $ids];

        $id = Http::withToken($tok)->post(self::API . '/tweets', $body)->throw()->json('data.id');
        return ['id' => $id, 'url' => "https://x.com/i/status/$id"];
    }

    private function uploadImage(string $tok, Media $m): string
    {
        $f = $this->tmp($m);
        try {
            return Http::withToken($tok)->attach('media', file_get_contents($f), basename($f))
                ->post(self::API . '/media/upload', ['media_category' => 'tweet_image'])
                ->throw()->json('data.id');
        } finally { @unlink($f); }
    }

    private function uploadVideo(string $tok, Media $m): string
    {
        $f = $this->tmp($m);
        try {
            $size = filesize($f);
            $id = Http::withToken($tok)->post(self::API . '/media/upload/initialize', [
                'media_type' => 'video/mp4', 'total_bytes' => $size, 'media_category' => 'tweet_video',
            ])->throw()->json('data.id');

            $h = fopen($f, 'r'); $i = 0;
            while (!feof($h) && ($chunk = fread($h, 4 * 1024 * 1024)) !== false && $chunk !== '') {
                Http::withToken($tok)->timeout(120)->attach('media', $chunk, 'chunk')
                    ->post(self::API . "/media/upload/$id/append", ['segment_index' => $i++])->throw();
            }
            fclose($h);

            $fin = Http::withToken($tok)->post(self::API . "/media/upload/$id/finalize")->throw()->json('data');
            for ($n = 0; isset($fin['processing_info']) && $n < 40; $n++) {
                if (($fin['processing_info']['state'] ?? '') === 'succeeded') break;
                if (($fin['processing_info']['state'] ?? '') === 'failed') throw new \RuntimeException('X video processing failed.');
                sleep(max(2, (int) ($fin['processing_info']['check_after_secs'] ?? 3)));
                $fin = Http::withToken($tok)->get(self::API . '/media/upload', ['command' => 'STATUS', 'media_id' => $id])->throw()->json('data');
            }
            return $id;
        } finally { @unlink($f); }
    }
}
LinkedIn
