Tag Archives: twitter

Intégrer le single sign-on Twitter avec Yapl4twitter dans symfony

Le but de ce mini tutorial est d’implémenter la fonctionnalité single sign-on de Twitter sur un site construit autour de Symfony. Pour ce faire, j’ai utilisé la bibliothèque Yapl4twitter (que j’ai développé).

On n’oubliera pas de consulter Sign in with twitter afin de mieux comprendre comment fonctionne cette fonctionnalité.

Un article où il y a plus de code que de texte, mais je pense que c’est assez clair. ;-)

Installer Yapl4twitter et le configurer

$ cd lib/vendor
$ git clone git://github.com/mycroft/yapl4twitter.git
[...]

On le rajoute dans config/autoload.yml pour que les classes soient préloadées:

autoload:
    twitter
:
        name
: twitter
        path
: %SF_LIB_DIR%/vendor/yapl4twitter
        recursive
: off

On va ensuite avoir besoin de clefs valide d’une application. On prendra donc soin d’en créer une sur https://dev.twitter.com/apps et de créer le config/app.yml adéquat:

all:
    base_url
:              http://mysite.com

    twitter
:
        consumer_key
:      <Consumer key>
        consumer_secret
:   <Consumer secret>

        request_token_url
: https://api.twitter.com/oauth/request_token
        access_token_url
:  https://api.twitter.com/oauth/access_token
        authorize_url
:     https://api.twitter.com/oauth/authorize
        authenticate_url
:  https://api.twitter.com/oauth/authenticate

Le module et ses actions dans symfony

On crée un module spécifique au login/logout. Mon application s’appelle « frontend », tout simplement:

$ php symfony generate:module frontend twitter

On aura dans ce module deux actions: login et logout. On n’oubliera pas d’en créer les routes dans apps/frontend/config/routing.yml:

login:
  url
: /login
  param
: { module: twitter, action: login }

logout
:
  url
: /logout
  param
: { module: twitter, action: logout }

Il ne reste plus qu’à implémenter les deux actions:

    public function executeLogin(sfWebRequest $request)
    {
        $oa = new OAuth(
                sfConfig::get('app_twitter_consumer_key'),
                sfConfig::get('app_twitter_consumer_secret')
        );

        // Premiere etape: Pas de parametre oauth_token: On ne vient pas de twitter.com
        // On fait donc la demande d'un request token et on redirige l'utilisateur vers
        // twitter.com.
        if(FALSE == $request->hasParameter('oauth_token'))
        {
            $request_token_url = sfConfig::get('app_twitter_request_token_url');

            $callback_url = sfConfig::get('app_base_url');
            $callback_url.= $this->getController()->genUrl('@login');

            $request_token = $oa->getRequestToken($request_token_url,
                                                  $callback_url);

            // On redirige vers l'authenticate_url qui permet de ne pas avoir
            // a accepter l'application une nouvelle fois.
            $next_url = sfConfig::get('app_twitter_authenticate_url');
            $next_url.= '?oauth_token=' . $request_token['oauth_token'];

            $this->redirect( $next_url );
        }
        // Seconde etape: On a un parametre oauth_token. On vient donc de twitter.com
        // On va recuperer a partir du oauth_token et oauth_verifier l'oauth_token_secret.
        else
        {
            $oauth_token = $request->getParameter('oauth_token');
            $oauth_verifier = $request->getParameter('oauth_verifier');

            $access_token = $oa->getAccessToken(sfConfig::get('app_twitter_access_token_url'),
                                                $oauth_token,
                                                $oauth_verifier);

            // On a le token, on enregistre dans la session le screen_name et
            $this->getUser()->setAttribute('screen_name', $access_token['screen_name']);
            $this->getUser()->setAttribute('oauth_token', $access_token['oauth_token']);
            $this->getUser()->setAttribute('oauth_token_secret', $access_token['oauth_token_secret']);
            $this->getUser()->setAuthenticated(TRUE);

            // On pourrait avoir une table ou l'on stocke ces informations pour plus tard:
            // $account = AccountTable::getInstance()->findOneByScreenName($access_token['screen_name']);
            // if( ! $account )
            // {
            //     $account = new Account();
            //     $account->screen_name = $access_token['screen_name'];
            //     $account->user_id = $access_token['user_id'];
            //     $account->oauth_token = $access_token['oauth_token'];
            //     $account->oauth_token_secret = $access_token['oauth_token_secret'];
            //     $account->save();
            // }

            // $this->getUser()->setAttribute('account_id', $account->id);

        // On est loggue, on retourne sur la page principale:
            $this->redirect('@homepage');
        }

        return sfView::NONE;
    }

    // logout: On invalide tout.
    public function executeLogout(sfWebRequest $request)
    {
        $this->getUser()->setAttribute('screen_name', NULL);
        $this->getUser()->setAttribute('oauth_token', NULL);
        $this->getUser()->setAttribute('oauth_token_secret', NULL);
        $this->getUser()->setAuthenticated(FALSE);

        return $this->redirect('@homepage');
    }

Monitorer un flux twitter en temps réel avec Node.js

Cet article présente rapidement comment, grâce aux APIs de Twitter, Node.Js et un peu d’html5 (websocket) on peut facilement en quelques lignes de codes monitorer une partie du flux twitter.

Le code présente une application que j’ai mise en ligne sur http://t.mkz.me/. Le code ne marche que si le navigateur supporte les WebSocket (Chrome 5 et +, Firefox 4, IE 9 et Safari 5 je crois, pas sûr ;-) )

Comment ça marche ?

Grace à twitter-node, une bibliothèque node.js, on va se connecter à un flux en streaming de Twitter, en particulier maintenant au flux ‘filter’ auquel on va demander tous les tweets inclus dans une zone géographie donnée. Ensuite, pour chaque websocket ouverte sur le node.js (via la bibliothèque node-websocket-server, que j’ai déjà utilisé dans un précédent article, par des clients (notre navigateur par l’intermédiaire d’un script javascript).

Comment le mettre en place:

On choppe la dernière version de node.js:

$ wget http://nodejs.org/dist/node-v0.2.5.tar.gz
$ tar xfz node-v0.2.5.tar.gz
$ cd node-v0.2.5
$ ./configure ; make ; cd ..
[...]

Mais aussi de node-websocket-server et de twitter-node.

$ git clone https://github.com/miksago/node-websocket-server.git
[...]
$ git clone https://github.com/technoweenie/twitter-node.git
[...]

$ export NODE_PATH=$(pwd)/node-websocket-server/lib:$(pwd)/twitter-node/lib
$

A ce moment, on a besoin du code du serveur. On remprend rapidement l’exemple donné par twitter-node et on l’adapte pour utiliser node-websocket-server. Ca nous donne grosso-modo (Notez qu’il faut modifier le login/password…). Dans l’exemple suivant, on va tracker les tweets postés avec des informations de geoloc aux USA, mais on pourrait aussi utiliser des mots (Le faire sur justin bieber est tout simplement royal à suivre):

// server.js
var TwitterNode = require('twitter-node').TwitterNode;
var ws          = require('ws/server');
var http        = require('http');
var sys         = require('sys');

var twit = new TwitterNode({
  user: 'twitter login',
  password: 'twitter password',
  // http://www.findlatitudeandlongitude.com/
  // SW,NE (long, lat, long, lat)
  // France:
  // locations: [ 1.40, 48.2, 3.56, 49.3 ]
  // Europe:
  //locations: [ -12, 36.1, 27.3, 62.44 ]
  // USA:
  locations: [ -170, 24.3, -44, 71 ]
  // on peut aussi tracker des mots:
  //  track: ['bieber', 'justin', 'justinbieber']
});

// L'action est 'filter'. On peut aussi mettre 'sample', etc.
twit.action = 'filter';

twit.addListener('error', function(error) {
  console.log(error.message);
  sys.puts(error.message);
});

// server http pour la websocket
var httpServer = http.createServer(function(req, res){});

// websocket
var server = ws.createServer({
  debug: false
}, httpServer);

server.addListener("connection", function(socket){

    // Fonction qui va nous servir pour envoyer chaque tweet:
    var func = function(tweet) {
        try {
            socket.write(JSON.stringify(tweet));
        }
        catch (e) {
            sys.puts("Socket write error");
        }
    };

    // Evenement qui va avoir lieu a chaque reception d'un tweet.
    twit.addListener('tweet', func);

    socket.addListener("end", function () {
        sys.puts("socket end");
        twit.removeListener('tweet', func);
        socket.end();
    });
});

// On lance le serveur. On notera qu'on le lance sur le port 8001.
server.listen(8001);

// On lance le flux
twit.stream();

sys.puts("Hi world.");

On n’a qu’à le lancer dans le shell:

$ ./node-v0.2.5/node server.js
Hi world.

A ce moment, il nous manque juste le code du client. Un peu d’HTML, on choppe jquery au passage et ça nous donne:

<!-- index.php -->
<html>
<head>
<script src="jquery-1.4.4.min.js" type="text/javascript"></script>
<script type="text/javascript">

$(document).ready(function(){
    // La websocket que l'on stocke "globalement"
    var ws;
    var num_tweet;

    if ("WebSocket" in window) {
        // Ou connecter la websocket (serveur node.js)
        ws = new WebSocket("ws://t.mkz.me:8001/service");

        ws.onopen = function() {};

        ws.onmessage = function (evt) {
            // Quand on recoit un message du serveur...:
            var received_msg = evt.data;
            var tweet = JSON.parse(evt.data);
            var content = tweet.text;
            num_tweet = num_tweet + 1;

            // A chaque tweet on l'affiche:
            if( ! tweet.retweeted_status )
            {
                $('#flux ul').prepend('<li><b>' + tweet.user.screen_name + '</b>: ' + content + '</li>');
                $('#flux ul li').slice(30).remove();
            }
            else
            {
                $('#rt ul').prepend('<li><b>' + tweet.user.screen_name + '</b>: ' + content + '</li>');
                $('#rt ul li').slice(10).remove();
            }

            $('#counter').text( num_tweet + ' tweets' );
        };

        ws.onclose = function() {};
    } else {
        // Pas de support WebSocket.
        $('#flux').append('You need a browser with websocket');
    }
});
</script>
</head>
<body>

<h2>Last tweet</h2>
<span id='counter'></span>
<div id='flux'>
<ul></ul>
</div>

<h2>Last RT</h2>
<div id='rt'>
<ul></ul>
</div>

</body>
</html>

Et voilà le résultat: http://t.mkz.me/.

L’authentification Oauth 1.0a avec Twitter

Tout commença sur un coup de tête. La lutte fut longue et dure. Et je pense que j’ai eu toutes les erreurs possibles et inimaginables quand j’ai décidé de me pencher sur l’API et l’authentification OAuth de Twitter.

J’ai voulu tout refaire from scratch, en PHP et avec curl pour faire les requêtes.

Alors, commençons par le commencement. Et le commencement, c’est sur la page qui décrit le process.

Pour faire simple, voici les étapes pour chopper votre passe partout pour user de l’api twitter sur le dos d’un utilisateur béta :

⇀ Un user se pointe sur votre site. Jusque là, rien de grandiose, mais il faut réclamer à twitter un request_token à usage unique.
⇀ Quand on a ce request_token, on redirige l’utilisateur sur twitter avec ce token. L’utilisateur se retrouve sur une page type « voulez vous autoriser l’application xxx ? »
⇀ Si il n’est pas trop bête pour cliquer sur « Allow », il se retrouve redirigé avec un oauth_token et un oauth_verifier qui vous serviront à récupérer le précieux sésame, l’oauth_token_secret.

Ca parait super simple. Sauf qu’il ne faut pas se mélanger les pinceaux.

On va avoir à faire à créer et utiliser des URI encodées. Un coup de lecture de la RFC (http://www.ietf.org/rfc/rfc3986.txt) et on pond la fonction qui va nous servir à encoder nos données:

    public function _urlencode_rfc3986($input)
    {
         return str_replace('+',' ',str_replace('%7E', '~', rawurlencode($input)));
    }

Et on sort aussi une fonction qui va nous sortir à faire les requêtes:

    public function request($url, $method = 'GET', $post_params = NULL, $headers = NULL)
    {
        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_HEADER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURL_HTTP_VERSION_1_1, TRUE);

        $http_headers = array();

        if($method == 'POST')
        {
            curl_setopt($ch, CURLOPT_POST, TRUE);
            $http_headers[] = 'Expect:';
        }

        if(NULL !== $post_params)
        {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post_params);

            $http_headers[] = 'Content-Type: application/x-www-form-urlencoded';
        }

        if(NULL !== $headers && is_array($headers))
        {
            $http_headers = array_merge($http_headers, $headers);
        }

        if(count($http_headers))
        {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $http_headers);
        }

        $response = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        curl_close($ch);

        unset($ch);

        return array('code' => $code, 'response' => $response);
    }

On notera bien qu’on pourra faire un POST sans $post_params.

Incorrect Signature

Revenons à Twitter. Déjà, la doc nous dit qu’il faut signer toutes les requêtes. Il nous donne le pseudo code:

httpMethod + « & » +
url_encode( base_uri ) + « & » +
sorted_query_params.each { | k, v |
url_encode ( k ) + « %3D » +
url_encode ( v )
}.join(« %26″)

On le convertit assez aisement en PHP:

    public function buildSignature($secret, $method, $url, $params)
    {
        $str = $method . '&';
        $str.= $this->_urlencode_rfc3986($url) . '&';

        ksort($params);
        $i = 0;

        $param_str = '';
        foreach($params as $key => $value)
        {
            $param_str .= $this->_urlencode_rfc3986($key) . '=' . $this->_urlencode_rfc3986($value);
            if(++$i != count($params))
                $param_str .= '&';
        }

        $str .= $this->_urlencode_rfc3986( $param_str );

        $signed = base64_encode(hash_hmac("SHA1", $str, $secret, true));

        return $signed;
    }

Et finalement, on aura besoin pour les requêtes de construire un header Authorization: OAuth… Voici la fonction:

    public function makeAuthorization($params)
    {
        $args = array();
        foreach($params as $key => $value)
        {
            $args[] = $this->_urlencode_rfc3986( $key ). '="' . $this->_urlencode_rfc3986($value) . '"';
        }

        $str = implode($args, ', ');

        return(array('Authorization: OAuth ' . $str . "\n"));
    }

Bon, ben là, on a un peu tout les outils nécessaires pour faire nos requêtes d’authentification puis utiliser l’API.

Oauth_verifier missing

On sort le grand jeu, avec les fonctions pour récupérer un request token puis l’access token secret:

    public function parseTokens($str)
    {
        $tokens = explode('&', $str);
        $token_arr = array();
        foreach($tokens as $token)
        {
            list($field, $value) = explode('=', $token);
            $token_arr[ $field ] = $value;
        }
        return $token_arr;
    }

    public function getRequestToken($request_token_url, $callback_url = NULL)
    {
        $method = 'GET';
    $nonce = sha1('nonce' + time());
        $params = array(
                    'oauth_consumer_key' => $this->consumer_key,
                    'oauth_nonce' => $nonce,
                    'oauth_signature_method' => 'HMAC-SHA1',
                    'oauth_timestamp' => time(),
                    'oauth_version' => '1.0',
                  );

        if(NULL !== $callback_url)
        {
            $params['oauth_callback'] = $callback_url;
        }

        $signature = $this->buildSignature($this->consumer_secret . '&', $method, $request_token_url, $params);

        $params['oauth_signature'] = $signature;

        $rep_arr = $this->request($request_token_url, $method, NULL, $this->makeAuthorization($params));
        $rep = $rep_arr['response'];

    return $this->parseTokens( $rep );
    }

    public function getAccessToken($access_token_url, $oauth_token, $oauth_verifier)
    {
        $method = 'GET';

        $nonce = sha1('nonce' + time());
        $params = array(
                    'oauth_consumer_key' => $this->consumer_key,
                    'oauth_nonce' => $nonce,
                    'oauth_signature_method' => 'HMAC-SHA1',
                    'oauth_token' => $oauth_token,
                    'oauth_timestamp' => time(),
                    'oauth_version' => '1.0',
                    'oauth_verifier' => $oauth_verifier
                );

        $signature = $this->buildSignature($this->consumer_secret . '&', $method, $access_token_url, $params);
        $params['oauth_signature'] = $signature;

        $rep_arr = $this->request($access_token_url, $method, NULL, $this->makeAuthorization($params));
        $rep = $rep_arr['response'];

        return $this->parseTokens( $rep );
    }

Failed to validate oauth signature and token twitter

Structurons le tout dans une classe dédiée (d’où les utilisations des $this), et créons un index.php pour l’utilisation:

<?php

$cs_key = '...';
$cs_secret = '...';

$request_token_url = 'https://api.twitter.com/oauth/request_token';
$access_token_url = 'https://api.twitter.com/oauth/access_token';
$authorize_url = 'https://api.twitter.com/oauth/authorize';

require_once('OAuth.class.php');

$oa = new OAuth($cs_key, $cs_secret);

// En utilisant une url de callback 'oob', vous obtiendrez un PIN pour compléter.
// Le PIN sera à considérer comme la valeur de l'oauth_verifier que l'on aurait obtenu
// si l'on avait utilisé une url de callback à la place.
$request_token = $oa->getRequestToken($request_token_url, 'oob');
echo "You must go on " . $authorize_url . '?oauth_token=' . $request_token['oauth_token'] . "\n";

echo "Please give PIN number: ";
$fp = fopen('php://stdin', 'r');
$pin = trim(fgets($fp));

$access_token = $oa->getAccessToken($access_token_url, $request_token['oauth_token'], $pin);
// On gardera précieusement le contenu de l'access_token.

Status is a duplicate

Ne reste plus qu’à l’utiliser. On crée une classe Twitter dédiée à l’API Twitter et une fonction pour mettre à jour son status:

<?php
require_once 'OAuth.class.php';

class Twitter
{
    private $consumer_key = NULL;
    private $consumer_secret = NULL;

    private $oauth_token = NULL;
    private $oauth_token_secret = NULL;

    public function Twitter($consumer_key, $consumer_secret, $oauth_token, $oauth_token_secret)
    {
        $this->consumer_key = $consumer_key;
        $this->consumer_secret = $consumer_secret;

        $this->oauth_token = $oauth_token;
        $this->oauth_token_secret = $oauth_token_secret;
    }

    public function update($message)
    {
        $url = 'http://api.twitter.com/1/statuses/update.json';
        $method = 'POST';

        $oa = new OAuth($this->consumer_key, $this->consumer_secret);
        $nonce = sha1('nonce' + time());
        $params = array(
                    'oauth_consumer_key' => $this->consumer_key,
                    'oauth_nonce' => $nonce,
                    'oauth_signature_method' => 'HMAC-SHA1',
                    'oauth_timestamp' => time(),
                    'oauth_version' => '1.0',
                    'oauth_token' => $this->oauth_token,
                  );

        $twitter_params = array('status' => $message);

        $params = array_merge($params, $twitter_params);

        $signature = $oa->buildSignature($this->consumer_secret . '&' . $this->oauth_token_secret, $method, $url, $params);
        $params['oauth_signature'] = $signature;

        $ret = $oa->request( $url . '?' . http_build_query($twitter_params, '', '&'), 'POST', NULL, $oa->makeAuthorization($params) );

        return $ret;
    }
};

On notera deux choses: L’utilisation de l’authentification OAuth pour chaque requête sur Twitter, et les paramètres qui sont passés dans l’URL et non dans le body du POST.

Et un script pour l’utiliser:

<?php
$cs_key = '...';
$cs_secret = '...';

$oauth_token = 'id-...';
$oauth_token_secret = '...';

require_once 'Twitter.class.php';

$twitter = new Twitter($cs_key, $cs_secret, $oauth_token, $oauth_token_secret);

$twitter->update('Hello world');

En espérant que ça soit utile …
Cadeau: j’ai mis le code sur github ! Vous pouvez dès maintenant consulter le source directement à l’adresse http://github.com/mycroft/YeyAnotherTwitterPhpLib.

Utiliser Mongodb pour collecter des tweets avec le sample du firehose

Dans le cadre de tests à mon travail, j’ai eu l’occasion de gouter à l’API de streaming twitter (enfin qu’à sa version de démonstration, samplée à uniquement 5% du traffic total). Le but de la maneouvre ici est de récupérer et de stocker le flux et d’opérer des requêtes ponctuelles sur la base (récupération de tweets d’un user donné, requêtes par localités, etc.).

Je vous livre ici les scripts

<?php

$sampleUrl = 'http://stream.twitter.com/1/statuses/sample.json';

$user = '** votre login twitter **';
$pass = '** votre password twitter **';

// Connexion sur la base Mongo
$mongo_cnx = new Mongo();
$db = $mongo_cnx->twitter;

$fp = fopen("http://" . $user . ":" . $pass . "@stream.twitter.com/1/statuses/sample.json", "r");

while($data = fgets($fp))
{
    // Récupération du tweet complet fourni par Twitter
    $tweet = json_decode(trim($data));
   
    // Si le tweet n'a pas d'auteur, on le drop. C'est le cas sur les effacements de tweets.
    if( ! isset($tweet->user) )
        continue;
   
    // On découpe le tweet et son user (qui seront tous deux stockés dans les deux tables suivantes):
    $tweet_user = get_object_vars($tweet->user);
    $tweet_mesg = get_object_vars($tweet);
    unset($tweet_mesg['user']);
   
    $tweet_mesg['user_id'] = $tweet_user['id'];
   
    // Stockage du tweet et de son auteur:
    $db->user->insert($tweet_user);
    $db->tweet->insert($tweet_mesg);
   
    echo "Tweet " . $tweet_mesg['id'] . " done.\n";
}

fclose($fp);

Et on le lance:

$ php fetch_twitter.php
...
Tweet 16774425803 done.
Tweet 16774426103 done.
Tweet 16774426201 done.
Tweet 16774426202 done.
Tweet 16774426200 done.
...

Pendant le runtime, on peut regarder évoluer la base:

$ ./bin/mongo
MongoDB shell version: 1.4.3
url: test
connecting to: test
type "help" for help
> use twitter
switched to db twitter
> db.tweet.find().count()
457811
> db.user.find().count()
457811
...

Les applications sont multiples, en voici une simple qui prend les dix derniers tweets, et remonte leurs auteurs:

<?php

$mongo_cnx = new Mongo();
$db = $mongo_cnx->twitter;

$count = $db->tweet->count();

echo "There is $count tweets.\n";

$cur = $db->tweet->find()->skip( $count - 10 );

function getUser($db, $user_id)
{
    $user = $db->user->findOne( array( 'id' => $user_id ) );
    return $user;
}

foreach($cur as $tweet)
{
    $user = getUser($db, $tweet['user_id']);
    echo "From " . $user['name'] . ": " . $tweet['text'] . "\n";
}

En voici une seconde rapide qui va remonter les tweets des utilisateurs ayant définie une langue « fr »:

<?php

$mongo_cnx = new Mongo();
$db = $mongo_cnx->twitter;

$cur = $db->user->find(array("lang" => "fr"));

foreach($cur as $user)
{
    $tweet_cur = $db->tweet->find(array("user_id" => $user['id']));
    foreach($tweet_cur as $tweet)
    {
        echo $tweet['text'] . "\n";
    }  
}