Instant Tweets for Any Website

Mar 14, 2011 / By Yanick Champoux

Tags: ,

Say theres a website you would like to tweet directly from. Not via a Twitter client, not using a service like Yoolink, not through a Firefox plugin. No, you really want to be able to have a honest to God “Tweet this” input field on the website itself. It’s a strange requirement, for sure, but it’s a mission that I’d been given a few days ago. Here’s how I dealt with it.

Step 1: Register the Twitter Application

My first step was to visit the Twitter dev site and register an application, so that the twitter add-on I’m creating will be able to authenticate with the Twitter web service (and ultimately be able to relay the tweets I’ll be feeding it). Following the instructions provided on the site, it only takes a few seconds to have a new application created, and its credentials in our grubby little hands.

Step 2: The Authentication / Posting Backend

While it could be possible to come up with a JavaScript-only solution for our task, it would mean that the application’s credentials would be contained within the JavaScript, and thus visible to anyone bothering to peek at the code. Security-wise, that’s majorly icky. So we need a small back-end to take care of the OAuth authentication business. For such a simple task, I called upon the might of Dancer:

package instatweets;

use Dancer ':syntax';

use Dancer::Plugin::Auth::Twitter;
use Dancer::Session;
use Dancer::Session::YAML;

auth_twitter_init();

# request of authentication,
# keep track of where we come from and pass it to Twitter
get '/authenticate' => sub {
    session( 'origin', params->{origin} );
    redirect auth_twitter_authenticate_url;
};

# authentication with Twitter succeeded, yay!
# stashing of tokens is done by D::P::A::T
# simply redirect whence we came from
get '/' => sub {
    my $url = session( 'origin' );
    redirect $url;
};

# returns a status of 200 if we are authenticated,
# 500 if not
get '/authenticated' => sub {
    Dancer::SharedData->response->status( 500 ) unless session 'access_token';
    'answer: success';
};

post '/tweet' => sub {
    twitter->access_token( session( 'access_token' ) );
    twitter->access_token_secret( session( 'access_token_secret' ) );
    twitter->update( params->{update} );
};

get '/fail' => sub {
    Dancer::SharedData->response->status( 500 );
    'FAIL';
};

true;

Most of the work is done by Dancer::Plugin::Auth::Twitter behind the scene. To have the user authenticated by Twitter, we have to hit the ‘/authenticate’ action, which will automatically redirect us to the Twitter authentication page. If the user confirms that he or she trusts the application, Twitter will bounce back to our ‘/’ action, which will automatically stash the user’s tokens in his or her session and complete the Great Circle of Authentication and redirect the user on the page where all the fuss started.

Two points of interest here.

First, it’s good to remember that what’s stored on our side is not the user’s Twitter credentials, but rather authentication tokens that can only work in tandem with our application’s token. Which means that even if our application token was leaked out (which we still don’t want to happen, mind you), the user wouldn’t be totally vulnerable — he or she could revoke permissions given to our application without impacting its global identity.

Second, although I’ve used Dancer::Session::YAML for convenience here, I could have made the backend lighter still and push all the session information to the browser itself by using Dancer::Session::Cookie.

Step 3: Shoe-horn the Twitter Interface on the Page

For the final part, I used my favorite page-mucker agent, Greasemonkey, enhanced with jQuery to make the mucking as painless as possible.

// ==UserScript==
// @name           instatweets
// @namespace      http://babyl.ca/instatweets
// @include        http://search.cpan.org/*
// @require        http://localhost:3000/javascripts/jquery.js
// @require        http://localhost:3000/javascripts/autoresize.jquery.min.js
// ==/UserScript==

gm_xhr_bridge();

var insta_root = 'http://localhost:3000';

function submitTweet () {
    $("#sending_tweet").show();
    $.post(
        insta_root + '/tweet',
        {
            'update': $('#twitter_status').get(0).value,
        },
        function ( data, textStatus ) {
            $("#sending_tweet").hide();
            $("#twitter_status").get(0).value = "";
            update_counter();
            $('#twitter_term').slideToggle();
        }
    );
};

function update_counter () {
    var l = $('#twitter_status').get(0).value.length;
    $('#twitter_counter')
        .html(l)
        .css('color', l > 140 ? 'red' : 'black' );
}

$(function(){
    $("<div id='twit' />" )
        .css({
            position:   "absolute",
            top:        "0px",
            right:      "5px"
        })
        .appendTo('body');

    $( '<img id="twitter_logo" src="' + insta_root + '/twitter_logo.png" />' )
        .appendTo( '#twit' )
        .click( function(){
                $('#twitter_term').slideToggle();
            });

$('body').append( '<div id="twitter_term" style="padding: 5px; z-index: 20000; display: none; background-color: lightgrey; position: absolute; top: 0px; right: 120px;";>'
+ '<form method="POST" id="tweet_form">'
+ '    <textarea id="twitter_status" name="status" style="width: 50em"></textarea>'
+ '    <input id="submit_tweet" type="button" value="tweet" />'
+ '</form>'
+ '<p>characters: <span id="twitter_counter"></span></p>'
+ '<div id="sending_tweet" style="display: none">sending...</div>'
+ ''
+ '<div id="twitter_warnings">'
+ '</div>'
+ ''
+ '<p align="right"><a href="#hide" onclick="$('#twitter_term').slideToggle();return false;">hide</a></p>'
+ '</div>');

    $('#twitter_status').autoResize();

    $('<span/>').attr('class','not_auth').html(
        "you must <a href='" + insta_root + "/authenticate?origin=" + document.location +"'>"
        + "authenticate</a> yourself " + "on Twitter before you can tweet"
    ).prependTo('#twitter_warnings');

    $.get( insta_root + '/authenticated',
        function(data) { $('.not_auth').hide(); } );

    $('#submit_tweet').click(submitTweet);

    $('#twitter_status').keyup(update_counter);
    update_counter();

});

We now have a Twitter box that will appears as soon as we click on the Tweet-bird (and neatly re-hide itself if we click again), will check all by itself if we are authenticated (and let us know of the link to remedy the situation if we aren’t). We also have the classic auto-updated character counter, and thanks to a nice jQuery plugin, the tweet input box resizes itself as we type more text. All of that in 80-something lines.

A little note here too: if you look at all the code on GitHub, you’ll see that the call to ‘gm_xhr_bridge()’ leds to a little more code fudging jQuery’s xhr object to use the ‘GM_xmlhttpRequest’, which magically allows to do cross-site scripting. Not very honorable, perhaps, but incredibly useful to know. This being said, the problem of cross-site scripting could also have been dealt with using JSONP callbacks, which in this instance could have been added to the overall solution in a few lines (thanks to Dancer).

And the Result…

… if applied to good ol’ search.cpan.org:

CPAN Twitterized

All the code, as usual, is available on GitHub.

Leave a Reply

  • (will not be published)

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>