13

I'm currently implementing a web smartphone application with Phonegap. On this application, users can post images they take with the phone camera on Facebook. This feature has been succesfully implemented only using javascript, by sending a base 64 encoded image. Now, I want to implement the same feature using Twitter.

I found some very interesting blog posts about this and I'm already be able to update the user status only using javascript... but I can't post images too using the update_with_media Twitter web service.

According too this post, someone says it's impossible to implement this operation without using a server side code (like a php script for example).

So my question is : is it possible to use the update_with_media Twitter web service only with javascript ?

I send you my code to have an overview of the current solution. I've taken this article as working base : http://oodlestechnologies.com/blogs/Twitter-integration-on-PhoneGap-using-ChildBrowser-and-OAuth-for-iOS-and-Android-Platforms

Here is my HTML code.

<!DOCTYPE html>
<html>
    <head>
        <title>Test</title>
        <script type="text/javascript" src="../js/jquery/jquery.min.js"></script>
        <script type="text/javascript" src="../cordova-2.5.0.js"></script>
        <script type="text/javascript" src="../js/childBrowser/childbrowser.js"></script>
        <script type="text/javascript" src="../js/helpers/jsOAuth-1.3.6.js"></script>
        <script type="text/javascript" src="../js/helpers/twitter.js"></script>
    </head>
    <body>
        <h4>Oodles Twitter App</h4>
        <table border="1">
            <tr>
                <th>Login using Twitter</th>
                <th>
                    <button id="loginBtn" onclick="Twitter.init();">Login</button>
                    <button id="logoutBtn" onclick="logOut();">Logout</button>
                </th>
            </tr>
            <tr id="tweetText">
                <td colspan="2"><textarea id="tweet"></textarea></td>
            </tr>
            <tr id="tweetBtn">
                <td colspan="2" align="right">
                    <button id="tweeter" onclick="Twitter.tweet();">Tweet</button>
                </td>
            </tr>
            <tr><td colspan="2"><div id="welcome">Please Login to use this app</div></td></tr>
        </table>
        <br/>
        <br/>
        <button onclick="javascript:location.reload();">Recharger la page</button>
    </body>
</html>

Here is my twitter.js code : (The point is in the post method)

$(document).ready(function() {
    document.addEventListener("deviceready", onDeviceReady, false);
});

function onDeviceReady() {
    var root = this;
    cb = window.plugins.childBrowser;
    if (!localStorage.getItem(twitterKey)) {
        $("#loginBtn").show();
        $("#logoutBtn").hide();
        $("tweetBtn").hide();
        $("tweetText").hide();
    }
    else {
        $("#loginBtn").hide();
        $("#logoutBtn").show();
        $("tweetBtn").show();
        $("tweetText").show();
    }

    if (cb != null) {
        cb.onLocationChange = function(loc) {
            root.locChanged(loc);
        };
        cb.onClose = function() {
            root.onCloseBrowser()
        };
        cb.onOpenExternal = function() {
            root.onOpenExternal();
        };
    }
}

function onCloseBrowser() {
    console.log("onCloseBrowser!");
}

function locChanged(loc) {
    console.log("locChanged!");
}

function onOpenExternal() {
    console.log("onOpenExternal!");
}

// Consumer key : ...
// Consumer secret : ...

// GLOBAL VARS
var oauth; // It Holds the oAuth data request
var requestParams; // Specific param related to request
var options = {consumerKey: '...', consumerSecret: '...', callbackUrl: "http://www.google.fr"};
var twitterKey = "twtrKey"; // This key is used for storing Information related   
var Twitter = {
    init: function() {
        // Apps storedAccessData , Apps Data in Raw format
        var storedAccessData, rawData = localStorage.getItem(twitterKey);
        // here we are going to check whether the data about user is already with us.
        if (localStorage.getItem(twitterKey) !== null) {
            // when App already knows data
            storedAccessData = JSON.parse(rawData); //JSON parsing
            //options.accessTokenKey = storedAccessData.accessTokenKey; // data will be saved when user first time signin
            options.accessTokenSecret = storedAccessData.accessTokenSecret; // data will be saved when user first first signin

            // javascript OAuth take care of everything for app we need to provide just the options
            oauth = OAuth(options);
            oauth.get('https://api.twitter.com/1/account/verify_credentials.json?skip_status=true',
                    function(data) {
                        var entry = JSON.parse(data.text);
                        console.log("USERNAME: " + entry.screen_name);
                    }
            );
        }
        else {
            // we have no data for save user
            oauth = OAuth(options);
            oauth.get('https://api.twitter.com/oauth/request_token',
                    function(data) {
                        requestParams = data.text;
                        cb.showWebPage('https://api.twitter.com/oauth/authorize?' + data.text); // This opens the Twitter authorization / sign in page
                        cb.onLocationChange = function(loc) {
                            Twitter.success(loc);
                        }; // Here will will track the change in URL of ChildBrowser
                    },
                    function(data) {
                        console.log("ERROR: " + JSON.stringify(data));
                    }
            );
        }
    },
    /*
     When ChildBrowser's URL changes we will track it here.
     We will also be acknowledged was the request is a successful or unsuccessful
     */
    success: function(loc) {

        // Here the URL of supplied callback will Load

        /*
         Here Plugin will check whether the callback Url matches with the given Url
         */
        if (loc.indexOf("http://www.google.fr") >= 0) {

            // Parse the returned URL
            var index, verifier = '';
            var params = loc.substr(loc.indexOf('?') + 1);

            params = params.split('&');
            for (var i = 0; i < params.length; i++) {
                var y = params[i].split('=');
                if (y[0] === 'oauth_verifier') {
                    verifier = y[1];
                }
            }

            // Here we are going to change token for request with token for access

            /*
             Once user has authorised us then we have to change the token for request with token of access
             here we will give data to localStorage.
             */
            oauth.get('https://api.twitter.com/oauth/access_token?oauth_verifier=' + verifier + '&' + requestParams,
                    function(data) {
                        var accessParams = {};
                        var qvars_tmp = data.text.split('&');
                        for (var i = 0; i < qvars_tmp.length; i++) {
                            var y = qvars_tmp[i].split('=');
                            accessParams[y[0]] = decodeURIComponent(y[1]);
                        }

                        $('#oauthStatus').html('<span style="color:green;">Success!</span>');
                        $('#stage-auth').hide();
                        $('#stage-data').show();
                        oauth.setAccessToken([accessParams.oauth_token, accessParams.oauth_token_secret]);

                        // Saving token of access in Local_Storage
                        var accessData = {};
                        accessData.accessTokenKey = accessParams.oauth_token;
                        accessData.accessTokenSecret = accessParams.oauth_token_secret;

                        // Configuring Apps LOCAL_STORAGE
                        console.log("TWITTER: Storing token key/secret in localStorage");
                        localStorage.setItem(twitterKey, JSON.stringify(accessData));

                        oauth.get('https://api.twitter.com/1/account/verify_credentials.json?skip_status=true',
                                function(data) {
                                    var entry = JSON.parse(data.text);
                                    console.log("TWITTER USER: " + entry.screen_name);
                                    $("#welcome").show();
                                    document.getElementById("welcome").innerHTML = "welcome " + entry.screen_name;
                                    successfulLogin();
                                    // Just for eg.
                                    app.init();
                                },
                                function(data) {
                                    console.log("ERROR: " + data);
                                }
                        );

                        // Now we have to close the child browser because everthing goes on track.

                        window.plugins.childBrowser.close();
                    },
                    function(data) {
                        console.log(data);


                    }
            );
        }
        else {
            // Just Empty
        }
    },
    tweet: function() {
        var storedAccessData, rawData = localStorage.getItem(twitterKey);

        storedAccessData = JSON.parse(rawData); // Paring Json 
        options.accessTokenKey = storedAccessData.accessTokenKey; // it will be saved on first signin
        options.accessTokenSecret = storedAccessData.accessTokenSecret; // it will be save on first login

        // javascript OAuth will care of else for app we need to send only the options
        oauth = OAuth(options);
        oauth.get('https://api.twitter.com/1/account/verify_credentials.json?skip_status=true',
                function(data) {
                    var entry = JSON.parse(data.text);
                    Twitter.post();
                }
        );
    },
    /*
     We now have the data to tweet
     */
    post: function() {
        alert('Post !');
        var theTweet = $("#tweet").val(); // You can change it with what else you likes.

        oauth.post('https://upload.twitter.com/1/statuses/update_with_media.json',
                {
                    'status': theTweet,
                    'media': //HERE IS THE PROBLEM, WHAT TO DO HERE ?
                }, "multipart/form-data",
                function(data)
                {
                    alert('Data 1 !');
                    console.log('------Data1 : ' + data);
                    var entry = JSON.parse(data.text);
                    console.log(entry);
                    done();
                },
                function(data) {
                    //var json_result = JSON.parse(data);
                    //alert(json_result.text.error);
                    var entry = JSON.stringify(data);
                    console.log('------Data2 : ' + entry);
                }
        );
    }

}

function done() {
    alert("OKKK !");
    $("#tweet").val('');
}


function successfulLogin() {
    $("#loginBtn").hide();
    $("#logoutBtn,#tweet,#tweeter,#tweetBtn,#tweetText").show();

}

function logOut() {
    //localStorage.clear();
    window.localStorage.removeItem(twitterKey);
    document.getElementById("welcome").innerHTML = "Please Login to use this app";
    $("#loginBtn").show();
    $("#logoutBtn,#tweet,#tweeter,#tweetText,#tweetBtn").hide();

}

After many tests (sending a base64 image, sending a blob, sending a binary file, ...) here is the return message from Twitter I have :

{\"errors\":[{\"message\":\"Internal error\",\"code\":131}]}","xml":"","requestHeaders":{"Content-Type":"multipart/form-data"},"responseHeaders":{"date":"Fri, 19 Apr 2013 15:45:28 GMT","content-encoding":"deflate","strict-transport-security":"max-age=631138519","status":"500 Internal Server Error","server":"tfe","content-type":"application/json; charset=utf-8","version":"HTTP/1.1"}}

A "solution" (by send a blob) have been posted on the Twitter dev forum but not working for me : dev.twitter.com/discussions/6969

Does anyone want to implement the same feature or have a solution ? Thank you !

------ EDITED :

I just want to use Javascript and I don't want to implement any server-side solution (no PHP, C#, Java...).

Antoine
  • 548
  • 4
  • 13
  • You are using the PhoneGap framework correct? You can look into building a plugin (http://docs.phonegap.com/en/2.7.0/guide_plugin-development_index.md.html#Plugin%20Development%20Guide) that allows you to hook a JavaScript action to native code. However, you'd be writing native code (ios, android, wp, etc) instead of writing a service wrapper (php, asp) in one language. I'd recommend, though it's not what you ask - to write a service wrapper that creates the file you need. – Markus May 06 '13 at 19:56
  • Been reading more about this. Have you looked into doing something like this `var pngData = canvas.toDataURL();` then you would need to trim the contents to just the data you are looking for. – Markus May 06 '13 at 20:18
  • The image I want to post is actually generated by converting a part of the HTML page to a HTML5 canvas and then, retrieving the base64 representation of the canvas from the toDataURL() method... – Antoine May 07 '13 at 07:28
  • Moreover, I don't want to write any Phonegap plugin to don't have to write new code for each new platform. I really want to use only Javascript. Thanks for your answers. – Antoine May 07 '13 at 07:30

2 Answers2

1

According to the docs, Twitter requires the multipart/form-data enctype, which means a base64 string isn't going to work.

Unlike POST statuses/update, this method expects raw multipart data. Your POST request's Content-Type should be set to multipart/form-data with the media[] parameter ~ https://dev.twitter.com/docs/api/1/post/statuses/update_with_media

However, you could host an endpoint that takes base64, converts it to a real file, and forwards the request to Twitter. For example (untested):

<?php

$base64 = $_POST['image'];
$data = base64_decode( $base64 );

// Make name unique to avoid conflicts.
$temp_file = uniqid() . $_POST['name'];

// Save the file to a temp location.
file_put_contents( $temp_file, $data );

$temp_info = pathinfo( $temp_file );
$temp_type = $temp_info['extension'];
$temp_name = basename( $temp_file, '.' . $temp_type );

// OAuth library recommended by Twitter: https://github.com/themattharris/tmhOAuth
// See original: https://github.com/themattharris/tmhOAuth-examples/blob/master/images.php

require 'tmhOAuth.php';
require 'tmhUtilities.php';

$tmhOAuth = new tmhOAuth( array(
    'consumer_key'    => $_POST['consumer_key'],
    'consumer_secret' => $_POST['consumer_secret'],
    'user_token'      => $_POST['user_token'],
    'user_secret'     => $_POST['user_secret'],
));

// note the type and filename are set here as well
// Edit: Not sure if the `type` and `filename` params are necessary.
$params = array( 'media[]' => "@{$temp_file};type={$temp_type};filename={$temp_name}" );

$code = $tmhOAuth->request( 'POST', $tmhOAuth->url( '1/status/update_with_media' ),
    $params,
    true, // use auth
    true  // multipart
);

// Remove temp file.
unlink( $temp_file );

if ( $code == 200 ) {
    tmhUtilities::pr( json_decode( $tmhOAuth->response['response'] ) );
}
tmhUtilities::pr( htmlentities( $tmhOAuth->response['response'] ) );

?>

And you might call it like:

    $.ajax({
        // You'll want to use https to protect the oauth info.
        url: "https://mysite.com/proxy.php",
        type: "POST",
        data: {
            image: "base64 data...",
            name: "foo.png",
            consumer_key: options.consumerKey,
            consumer_secret: options.consumerSecret,
            user_token: options.accessTokenKey,
            user_secret: options.accessTokenSecret
        },
        success: function( data ) {
            console.log( data );
        }
    });
Stephen Bunch
  • 327
  • 4
  • 15
  • I don't want to use any server-side code to do this. I have already implemented a C# solution that perfectly works with a real file like you do in PHP, but it's not the point. I just want to use Javascript code. – Antoine May 03 '13 at 12:06
  • 1
    Assuming you're not using `input` elements to upload files and you only have base64 data to work with, I believe what you're trying to do is impossible given that browsers don't currently have a way of converting a base64 string to a file upload. If you were using an input element, you could use the iframe technique or send the data directly (only HTML5 browser), but that's not the case. – Stephen Bunch May 03 '13 at 16:37
  • 1
    I don't use any `input` element because I want to post a "generated" image on Twitter. The user don't select the image in its gallery or the phone file system. The image is generated by converting a part of the HTML page to a HTML5 canvas and then, retrieving the base64 representation of the canvas from the `toDataURL()` method. Any idea with this information ? – Antoine May 06 '13 at 07:52
  • 1
    Yeah, it's a pain. I've wanted to do the same thing. The problem is the server is expecting a file, not the contents of a file, so you'll still need server-side code to generate the physical file. For security reasons, HTML5 isn't allowed to interact with the file system. – Stephen Bunch May 06 '13 at 16:56
  • Would ".readAsBinaryString()" solve your problems? See a similar discussion on twitter: https://dev.twitter.com/discussions/4200 – Markus May 06 '13 at 20:11
  • why are you avoid using a server side? what is the main reason? – ncubica May 08 '13 at 19:55
0

For anyone trying to post images to Twitter using client JS, I was able to post to twitter using the solution by gary-buynary-co-za (https://github.com/bytespider/jsOAuth/pull/11) at the end of this forum. Pretty much ended up using Phonegap FileTransfer and FileTransferOptions objects for transferring image to twitter api, but used jsOAuth for preparing FileTransferOptions headers and signatures. The solution could definitely be cleaned up though.

TheBigC
  • 605
  • 3
  • 9
  • 17