Media_ids parameter ignored with status update

media
media-upload

#1

Hi everyone!

I wrote my own PHP code to publish statuses from my website through the REST API. Everything is working like a charm except when I want to publish with the media_ids parameter, which seems being ignored when using the status endpoint.

I tried everything I could, adding my own owner ID with the additional_owners parameter but with no luck. I don’t understand where my code failed.

You’ll find below my snippet code to call the API

<?php
function Twitter_api_call($array)
{
    $time = time();
    $action = $array['action'];
    if ($action == 'view' || $action == 'get' || $action == 'read') {
        $action = 'show';
    } elseif ($action == 'post') {
        $action = 'update';
    } elseif ($action == 'delete') {
        $action = 'destroy';
    } elseif ($action == 'create_metadata') {
        $action = 'metadata';
    }
    
    $consumer_key = $array['consumer_key'];
    $consumer_secret = $array['consumer_secret'];
    $access_token = $array['access_token'];
    $access_token_secret = $array['access_token_secret'];
    
    if (is_null($action) || empty($action)) {
        exit('Action is not set');
    } if (is_null($consumer_key) || empty($consumer_key)) {
        exit('Consumer key is not set');
    } if (is_null($consumer_secret) || empty($consumer_secret)) {
        exit('Consumer secret is not set');
    } if (is_null($access_token) || empty($access_token)) {
        exit('Access token is not set');
    } if (is_null($access_token_secret) || empty($access_token_secret)) {
        exit('Access token secret is not set');
    }
    
    /* API calls */
    $api_url['timeline'] = 'https://api.twitter.com/1.1/statuses/user_timeline.json';
    $api_url['update'] = 'https://api.twitter.com/1.1/statuses/update.json';
    $api_url['destroy'] = 'https://api.twitter.com/1.1/statuses/destroy/' . $array['id'] . '.json';
    $api_url['upload'] = 'https://upload.twitter.com/1.1/media/upload.json';
    $api_url['metadata'] = 'https://upload.twitter.com/1.1/media/metadata/create.json';
    $api_url['show'] = 'https://api.twitter.com/1.1/statuses/show/' . $array['id'] . '.json';
    
    /* curl URL */
    $curl_url = $api_url[$action];
    
    /* Method */
    $method = 'GET';
    if ('metadata' == $action || 'update' == $action || 'upload' == $action || 'destroy' == $action) {
        $method = 'POST';
    }
    
    /* OAuth fields */
    $oauth_fields['oauth_consumer_key'] = $consumer_key;
    $oauth_fields['oauth_nonce'] = $time;
    $oauth_fields['oauth_signature_method'] = 'HMAC-SHA1';
    $oauth_fields['oauth_timestamp'] = $time;
    $oauth_fields['oauth_token'] = $access_token;
    $oauth_fields['oauth_version'] = '1.0';
    
    /* Signature */
    $oauth_hash = '';
    foreach ($oauth_fields as $key => $val) {
        $oauth_hash .= $key . '=' . $val . '&';
    }
    $oauth_hash = trim($oauth_hash, ' &');
    
    $base = $method
          . '&' . rawurlencode($api_url[$action])
          . '&' . rawurlencode($oauth_hash);
    
    $key = rawurlencode($consumer_secret)
         . '&' . rawurlencode($access_token_secret);
    
    $hash_hmac = hash_hmac('sha1', $base, $key, true);
    
    $signature = base64_encode($hash_hmac);
    $signature = rawurlencode($signature);
    
    /* cURL header */
    $oauth_fields['oauth_signature'] = $signature;
    ksort($oauth_fields);
    
    $oauth_header = '';
    foreach ($oauth_fields as $key => $val) {
        $oauth_header .= $key . '="' . $val . '", ';
    }
    $oauth_header = trim($oauth_header, ' ,');
    
    $curl_header[] = 'Authorization: OAuth ' . $oauth_header;
    
    $curl_opt[CURLOPT_URL] = $curl_url;
    $curl_opt[CURLOPT_SSL_VERIFYPEER] = false;
    $curl_opt[CURLOPT_RETURNTRANSFER] = true;
    $curl_opt[CURLOPT_HEADER] = false;
    
    if ('update' == $action || 'upload' == $action) {
        $curl_opt[CURLOPT_POST] = true;
        
        if ('upload' == $action) {
            $opt['media'] = file_get_contents($array['media']);
            
            if (!is_null($array['additional_owners'])) {
                $opt['additional_owners'] = $array['additional_owners'];
            }
        } else {
            $opt['status'] = $array['status'];
            
            if (!empty($array['media_ids'])) {
                $opt['media_ids'] = $array['media_ids'];
            }
        }
        
        $curl_opt[CURLOPT_POSTFIELDS] = $opt;
    } elseif ('metadata' == $action) {
        $curl_opt[CURLOPT_POST] = true;
        $curl_header[] = 'Content-Type: application/json';
        
        $json = json_encode([
            'media_id' => $array['media_id'],
            'alt_text' => [
                'text' => $array['alt_text']
            ]
        ]);
        
        $curl_opt[CURLOPT_POSTFIELDS] = $json;
    }
    
    $curl_opt[CURLOPT_HTTPHEADER] = $curl_header;
    
    $ch = curl_init();
    curl_setopt_array($ch, $curl_opt);
    $json = curl_exec($ch);
    
    return $json;
}

What are my steps:

  • Uploading the media file:
Twitter_api_call([
    'consumer_key' => $consumer_key,
    'consumer_secret' => $consumer_secret,
    'access_token' => $access_token,
    'access_token_secret' => $access_token_secret,

    'action' => 'upload',
    'media' => 'path_to_file'
]);

The server response looks all good:

{
    "media_id": 993508580614602753,
    "media_id_string": "993508580614602753",
    "size":415253,
    "expires_after_secs":86400,
    "image": {
        "image_type": "image\/jpeg",
        "w":1707,
        "h":1280
    }
}
  • Retrieving the media_id_string return, then posting status:
Twitter_api_call([
    'consumer_key' => $consumer_key,
    'consumer_secret' => $consumer_secret,
    'access_token' => $access_token,
    'access_token_secret' => $access_token_secret,

    'action' => 'update',
    'media_ids' => $media_id_string,
    'status' => 'My new tweet with an attached media!'
]);

And the response from the status:

{
    "created_at": "Mon May 07 15: 32: 28 +0000 2018",
    "id": 993513906030239745,
    "id_str": "993513906030239745",
    "text": "My new tweet with an attached media!",
    "truncated": true,
    "entities": {
        "hashtags": [],
        "symbols": [],
        "user_mentions": [],
        "urls": [
            {
                "url": "http:\/\/t.co\/MwRajL5w3X",
                "expanded_url": "http:\/\/twitter.com\/i\/web\/status\/993513906030239745",
                "display_url": "twitter.com\/i\/web\/status\/9\u2026",
                "indices": [117,140]
            }
        ]
    },
    "source": "<a href=\"http:\/\/www.coasterrider.fr\" rel=\"nofollow\">Coasterrider<\/a>",
    "in_reply_to_status_id": null,
    "in_reply_to_status_id_str": null,
    "in_reply_to_user_id": null,
    "in_reply_to_user_id_str": null,
    "in_reply_to_screen_name": null,
    "user": {
        "id": ...,
        "id_str": "...",
        "name": "Coasterrider",
        "screen_name": "CoasterriderFR",
        "location": "",
        "description": "Roller coasters, parcs, attractions et divertissement au rapport depuis 2004",
        "url": "http:\/\/t.co\/cTsSmPTzmd",
        "entities": {
            "url": {
                "urls": [
                    {
                        "url": "http:\/\/t.co\/cTsSmPTzmd",
                        "expanded_url": "http:\/\/www.coasterrider.fr",
                        "display_url": "coasterrider.fr",
                        "indices": [0,23]
                    }
                ]
            },
            "description": {
                "urls": []
            }
        },
        "protected": false,
        "followers_count": 5,
        "friends_count": 0,
        "listed_count": 0,
        "created_at": "Wed Sep 07 05: 39: 09 +0000 2011",
        "favourites_count": 0,
        "utc_offset": 7200,
        "time_zone": "Brussels",
        "geo_enabled": true,
        "verified": false,
        "statuses_count": 1,
        "lang": "fr",
        "contributors_enabled": false,
        "is_translator": false,
        "is_translation_enabled": false,
        "profile_background_color": "C0DEED",
        "profile_background_image_url": "http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png",
        "profile_background_image_url_https": "http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png",
        "profile_background_tile": false,
        "profile_image_url": "http:\/\/pbs.twimg.com\/profile_images\/992174072086835200\/pz4Y1Yut_normal.jpg",
        "profile_image_url_https": "http:\/\/pbs.twimg.com\/profile_images\/992174072086835200\/pz4Y1Yut_normal.jpg",
        "profile_banner_url": "http:\/\/pbs.twimg.com\/profile_banners\/369337898\/1525387292",
        "profile_link_color": "1DA1F2",
        "profile_sidebar_border_color": "C0DEED",
        "profile_sidebar_fill_color": "DDEEF6",
        "profile_text_color": "333333",
        "profile_use_background_image": true,
        "has_extended_profile": false,
        "default_profile": true,
        "default_profile_image": false,
        "following": false,
        "follow_request_sent": false,
        "notifications": false,
        "translator_type": "none"
    },
    "geo": null,
    "coordinates": null,
    "place": null,
    "contributors": null,
    "is_quote_status": false,
    "retweet_count": 0,
    "favorite_count": 0,
    "favorited": false,
    "retweeted": false,
    "lang": "fr"
}

As you can see, I don’t get any errors from the calls, but the status is still posted, just without the media attached to it.

I tried to follow the suggestions from this topic, this one, this one or this one but haven’t found solutions to my problem…

That’s why I’m coming to you, any helps and clues are welcomed! Many thanks :slight_smile:


#2

My guess is that the payload isn’t correct.
The media ids need to be in a separate object than the status update.

If I were you I’d log everything I could (headers, body, url etc) and do the same with twurl and see the differences.


#3

Thanks for this first clue. I’ll edit this message when I’ll have a try these next days.

In my first examples, the media_ids parameter was already in the status update which is separated from the media upload call.


#4

After two days of fighting with the headers, I just found why the media_ids parameter was ignored.

It’s because when updating a status, I didn’t include the “status” parameters nor the “media_ids” in the base string for the hash.

So what I just did was adding the proper post fields in addition of the OAuth

<?php
/* OAuth fields for base string */
$base_fields['oauth_consumer_key'] = $consumer_key;
$base_fields['oauth_nonce'] = $time;
$base_fields['oauth_signature_method'] = 'HMAC-SHA1';
$base_fields['oauth_timestamp'] = $time;
$base_fields['oauth_token'] = $access_token;
$base_fields['oauth_version'] = '1.0';

$base_fields['status'] = $array['status'];
$base_fields['media_ids'] = $array['media_ids'];

ksort($base_fields);

$base_hash = '';
foreach ($base_fields as $key => $val) {
    $key = rawurlencode($key);
    $val = rawurlencode($val);
    
    $base_hash .= $key . '=' . $val . '&';
}
$base_hash = trim($base_hash, ' &');

$base = $method
      . '&' . rawurlencode($api_url[$action])
      . '&' . rawurlencode($base_hash);

$key = rawurlencode($consumer_secret)
     . '&' . rawurlencode($access_token_secret);

$hash_hmac = hash_hmac('sha1', $base, $key, true);
$signature = base64_encode($hash_hmac);

To be sure I built correctly the authorization header for the call, I had to filter the OAuth fields only (to not include post fields like media_ids or status):

<?php
$oauth_fields_header = [
    'oauth_consumer_key' => $base_fields['oauth_consumer_key'],
    'oauth_nonce' => $base_fields['oauth_nonce'],
    'oauth_signature_method' => $base_fields['oauth_signature_method'],
    'oauth_timestamp' => $base_fields['oauth_timestamp'],
    'oauth_token' => $base_fields['oauth_token'],
    'oauth_version' => $base_fields['oauth_version'],
    'oauth_signature' => $signature
];

$oauth_header = '';
foreach ($oauth_fields_header as $key => $val) {
    $oauth_header .= $key . '="' . rawurlencode($val) . '", ';
}
$oauth_header = trim($oauth_header, ' ,');

$curl_header[] = 'Authorization: OAuth ' . $oauth_header;
$curl_header[] = 'Expect:';

Then, to avoid the “Could not authenticate you” error for the server response, I had to http_build_query the CURL_POSTFIELDS when updating the status:

<?php
/*
$opt['status'] = 'Tweet status';
$opt['media_ids'] = '1234132323';
*/
$curl_opt[CURLOPT_POSTFIELDS] = http_build_query($opt, '', '&');

instead of

<?php
$curl_opt[CURLOPT_POSTFIELDS] = $opt;

Hope I brought some clues for the ones who met the same issue I did.

Here’s the full working code if you want to use it as a simple function - you can make it evolve as you want for your uses:

<?php
function Twitter_api_call($array)
{
    /*
        consumer_key
        consumer_secret
        access_token
        access_token_secret
        
        action
        
        status
        alt_text
        
        media
        media_id
        media_ids
        additional_owners
        
        id
        include_entities
    */
    
    $time = time();
    
    $action = $array['action'];
    if ($action == 'view' || $action == 'get' || $action == 'read') {
        $action = 'show';
    } elseif ($action == 'post') {
        $action = 'update';
    } elseif ($action == 'delete') {
        $action = 'destroy';
    } elseif ($action == 'create_metadata') {
        $action = 'metadata';
    }
    
    $consumer_key = $array['consumer_key'];
    $consumer_secret = $array['consumer_secret'];
    $access_token = $array['access_token'];
    $access_token_secret = $array['access_token_secret'];
    
    if (is_null($action) || empty($action)) {
        exit('Action is not set');
    } if (is_null($consumer_key) || empty($consumer_key)) {
        exit('Consumer key is not set');
    } if (is_null($consumer_secret) || empty($consumer_secret)) {
        exit('Consumer secret is not set');
    } if (is_null($access_token) || empty($access_token)) {
        exit('Access token is not set');
    } if (is_null($access_token_secret) || empty($access_token_secret)) {
        exit('Access token secret is not set');
    }
    
    /* API calls */
    $api_url['timeline'] = 'https://api.twitter.com/1.1/statuses/user_timeline.json';
    $api_url['update'] = 'https://api.twitter.com/1.1/statuses/update.json';
    $api_url['destroy'] = 'https://api.twitter.com/1.1/statuses/destroy/' . $array['id'] . '.json';
    $api_url['upload'] = 'https://upload.twitter.com/1.1/media/upload.json';
    $api_url['metadata'] = 'https://upload.twitter.com/1.1/media/metadata/create.json';
    $api_url['show'] = 'https://api.twitter.com/1.1/statuses/show/' . $array['id'] . '.json';
    
    /* curl URL */
    $curl_url = $api_url[$action];
    
    /* Method */
    $method = 'GET';
    if ($action == 'metadata' || $action == 'update' || $action == 'upload' || $action == 'destroy') {
        $method = 'POST';
    }
    
    /* OAuth fields for base string */
    $base_fields['oauth_consumer_key'] = $consumer_key;
    $base_fields['oauth_nonce'] = $time;
    $base_fields['oauth_signature_method'] = 'HMAC-SHA1';
    $base_fields['oauth_timestamp'] = $time;
    $base_fields['oauth_token'] = $access_token;
    $base_fields['oauth_version'] = '1.0';
    
    /* Signature */
    if ($action == 'update') {
        $base_fields['status'] = $array['status'];
        $base_fields['include_entities'] = 'true';
        
        if (!is_null($array['media_ids'])) {
            $base_fields['media_ids'] = $array['media_ids'];
        }
    }
    ksort($base_fields);
    
    $base_hash = '';
    foreach ($base_fields as $key => $val) {
        $key = rawurlencode($key);
        $val = rawurlencode($val);
        
        $base_hash .= $key . '=' . $val . '&';
    }
    $base_hash = trim($base_hash, ' &');
    
    $base = $method
          . '&' . rawurlencode($api_url[$action])
          . '&' . rawurlencode($base_hash);
    
    $key = rawurlencode($consumer_secret)
         . '&' . rawurlencode($access_token_secret);
    
    $hash_hmac = hash_hmac('sha1', $base, $key, true);
    $signature = base64_encode($hash_hmac);
    
    /* cURL header */
    $oauth_fields_header = [
        'oauth_consumer_key' => $base_fields['oauth_consumer_key'],
        'oauth_nonce' => $base_fields['oauth_nonce'],
        'oauth_signature_method' => $base_fields['oauth_signature_method'],
        'oauth_timestamp' => $base_fields['oauth_timestamp'],
        'oauth_token' => $base_fields['oauth_token'],
        'oauth_version' => $base_fields['oauth_version'],
        'oauth_signature' => $signature
    ];
    
    $oauth_header = '';
    foreach ($oauth_fields_header as $key => $val) {
        $oauth_header .= $key . '="' . rawurlencode($val) . '", ';
    }
    $oauth_header = trim($oauth_header, ' ,');
    
    $curl_header[] = 'Authorization: OAuth ' . $oauth_header;
    $curl_header[] = 'Expect:';
    
    $curl_opt[CURLOPT_HTTPHEADER] = $curl_header;
    $curl_opt[CURLOPT_HEADER] = false;
    $curl_opt[CURLOPT_URL] = $curl_url;
    $curl_opt[CURLOPT_RETURNTRANSFER] = true;
    $curl_opt[CURLOPT_TIMEOUT] = 10;
    
    if ($action == 'update' || $action == 'upload') {
        if ($action == 'upload') {
            $opt['media'] = file_get_contents($array['media']);
            
            if (!is_null($array['additional_owners'])) {
                $opt['additional_owners'] = $array['additional_owners'];
            }
            
            $curl_opt[CURLOPT_POSTFIELDS] = $opt;
        } else {
            $opt['status'] = $array['status'];
            $opt['include_entities'] = 'true';
            
            if (!empty($array['media_ids'])) {
                $opt['media_ids'] = $array['media_ids'];
            }
            
            $curl_opt[CURLOPT_POSTFIELDS] = http_build_query($opt, '', '&');
        }
    } elseif ($action == 'metadata') {
        $json = json_encode([
            'media_id' => $array['media_id'],
            'alt_text' => [
                'text' => $array['alt_text']
            ]
        ]);
        
        $curl_opt[CURLOPT_POSTFIELDS] = $json;
    }
    
    $ch = curl_init();
    curl_setopt_array($ch, $curl_opt);
    $json = curl_exec($ch);
    
    return $json;
}

Twitter_api_call([
    'consumer_key' => '',
    'consumer_secret' => '',
    'access_token' => '',
    'access_token_secret' => '',
    
    'action' => 'update',
    'media_ids' => 'your_media_ids',
    
    'status' => "Hi! I'm tweeting!"
]);

Thanks bobber205 for suggesting me trying another library too - this was the inspiration for making me suceed!