From 4767fe01095e6d5b4192345e371df3f302d1ba30 Mon Sep 17 00:00:00 2001 From: Bashy Date: Thu, 4 Jun 2026 19:35:03 +0300 Subject: [PATCH] Fix image posting for Discord and Mastodon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discord: payload_json was missing the attachments array, so uploaded files were orphaned and not shown in the message (Discord API v10 requirement). Mastodon: media_ids[] sent as URL-encoded form was unreliable — switch to JSON body. Also poll /api/v1/media/:id when upload returns 202 (async) to wait until the image is processed before attaching it. Co-Authored-By: Claude Sonnet 4.6 --- bin/dispatch-social.php | 43 ++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/bin/dispatch-social.php b/bin/dispatch-social.php index 7e85f0b..8d58804 100755 --- a/bin/dispatch-social.php +++ b/bin/dispatch-social.php @@ -86,7 +86,7 @@ function ensure_li_token(string $base, array &$config): ?string { return $li['access_token']; } -// Upload image to Mastodon, return media_id or null +// Upload image to Mastodon, return media_id or null (polls until ready if async) function mastodon_upload_image(string $api_base, string $token, string $image_path): ?string { if (!file_exists($image_path)) return null; $ch = curl_init($api_base . '/api/v2/media'); @@ -99,7 +99,25 @@ function mastodon_upload_image(string $api_base, string $token, string $image_pa $resp = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); - if ($status === 200 || $status === 202) return $resp['id'] ?? null; + + if ($status === 200) return $resp['id'] ?? null; + + // 202 = media accepted but still processing — poll until URL is set (max 10s) + if ($status === 202) { + $media_id = $resp['id'] ?? null; + if (!$media_id) return null; + for ($i = 0; $i < 10; $i++) { + sleep(1); + $ch2 = curl_init($api_base . '/api/v1/media/' . $media_id); + curl_setopt_array($ch2, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token]]); + $poll = json_decode(curl_exec($ch2), true); + curl_close($ch2); + if (!empty($poll['url'])) return $media_id; + } + log_msg(' Mastodon media still processing after 10s — attaching anyway'); + return $media_id; + } + log_msg(' Mastodon media upload HTTP ' . $status); return null; } @@ -150,13 +168,20 @@ function linkedin_upload_image(string $token, string $author, string $image_path // Discord multipart post (with optional file attachment) function discord_post(string $webhook_url, string $text, ?string $image_path): array { - $boundary = '----FormBoundary' . bin2hex(random_bytes(8)); + $boundary = '----FormBoundary' . bin2hex(random_bytes(8)); + $has_file = $image_path && file_exists($image_path); + $filename = $has_file ? basename($image_path) : null; + + // payload_json must declare attachments array so Discord links the uploaded file + $payload = ['content' => $text]; + if ($has_file) $payload['attachments'] = [['id' => 0, 'filename' => $filename]]; + $body = "--$boundary\r\n"; $body .= "Content-Disposition: form-data; name=\"payload_json\"\r\n\r\n"; - $body .= json_encode(['content' => $text]) . "\r\n"; - if ($image_path && file_exists($image_path)) { + $body .= json_encode($payload) . "\r\n"; + if ($has_file) { $body .= "--$boundary\r\n"; - $body .= 'Content-Disposition: form-data; name="files[0]"; filename="' . basename($image_path) . '"' . "\r\n"; + $body .= 'Content-Disposition: form-data; name="files[0]"; filename="' . $filename . '"' . "\r\n"; $body .= 'Content-Type: ' . (mime_content_type($image_path) ?: 'image/jpeg') . "\r\n\r\n"; $body .= file_get_contents($image_path) . "\r\n"; } @@ -229,13 +254,13 @@ foreach ($targets as $t) { if ($mid) $media_ids[] = $mid; } $params = ['status' => $content]; - if ($media_ids) $params['media_ids[]'] = $media_ids[0]; + if ($media_ids) $params['media_ids'] = $media_ids; $ch = curl_init($api_base . '/api/v1/statuses'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, - CURLOPT_POSTFIELDS => http_build_query($params), - CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/x-www-form-urlencoded'], + CURLOPT_POSTFIELDS => json_encode($params), + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Content-Type: application/json'], ]); $resp = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);