Category Archives: Internet

Ecrire un bot IRC grâce à Python et Twisted

Quand il faut écrire une application réseau, les développeurs Python n’ont qu’un mot à la bouche: Twisted. Il s’agit d’un moteur réseau « event-driven », et comprend un grand nombre de protocoles de niveau 3 (TCP, UDP, socket unix) et 7 (HTTP, SSH, IMAP, IRC…).

On va donc simplement voir ici comment coder un bot irc (quasi) complet. On prend la doc « Writing Clients » et celle de la classe irc.IRCClient.

On a donc besoin d’écrire une classe de base pour le bot, et sa factory utilisable dans le reactor de twisted.

#!/usr/bin/env python

import sys

from twisted.words.protocols import irc
from twisted.internet import protocol
from twisted.internet import reactor

class QuickBot(irc.IRCClient):
    def _get_nickname(self):
        return self.factory.nickname
    nickname = property(_get_nickname)

    def signedOn(self):
        self.join(self.factory.channel)
        pass

    def privmsg(self, user, channel, msg):
        pass

class QuickBotFactory(protocol.ClientFactory):
    protocol = QuickBot

    def __init__(self, channel, nickname):
        self.channel = channel
        self.nickname = nickname

    def clientConnectionLost(self, connector, reason):
        connector.connect()

    def clientConnectionFailed(self, connector, reason):
        connector.connect()

Et le point d’entrée:

if __name__ == "__main__":
    reactor.connectTCP('localhost',
                       6667,
                       QuickBotFactory('#bot', 'quicky'))
    reactor.run()

Et du coup, j’ai un peu étendu le truc et j’ai créé Another Python Bot Using The Twisted Framework, aka Butt, avec un système de modules chargeables dynamiquement (avec imp) et l’utilisation de python-yaml pour les fichiers de conf. Un bon exemple pour commencer un bot IRC ! Les sources sont disponibles sur le projet Butt sur mon compte Github.

Apache: Lire et découper le corps d’une requête POST dans un module Apache

Le code est très inspiré du chapitre « Reading the Request body » du livre Writing Apache Modules with Perl and C.

Pour expérimenter un peu les modules Apache (suite à un nouvel entretien d’embauche), je me suis mis en tête de coder un mini service via un module pour générer des nombres aléatoire.

Le service devra être très simple et sera exécuté lors de l’appel de l’url http://localhost/randomint, et l’on pourra l’appeler en POST en donnant, par exemple, l’argument count pour préciser le nombre d’entiers aléatoires que l’on désire.

Et finalement, l’étape la plus complexe aura été de reconstruire le body envoyé lors de la requête POST, et de le parser pour en sortir un tableau associatif (key/value).

Reconstruction du body

Cette fonction attend l’envoie du body par le client et reconstruit en un seul block toutes les données envoyées.

static int chunk_reader(request_rec *r, const char **rbuf)
{
    int ret;

    if(OK != (ret = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR))) {
        return ret;
    }

    if(ap_should_client_block(r)) {
        char argsbuffer[HUGE_STRING_LEN];
        int rsize, len_read, rpos=0;
        long length = r->remaining;

        *rbuf = apr_pcalloc(r->pool, length + 1);

        while(0 < (len_read = ap_get_client_block(r, argsbuffer, sizeof(argsbuffer)))) {
            if ((rpos + len_read) > length) {
                rsize = length - rpos;
            }
            else {
                rsize = len_read;
            }

            memcpy((char*)*rbuf + rpos, argsbuffer, rsize);
            rpos += rsize;
        }
    }

    return ret;
}

Création du tableau associatif

Avec le block recomposé, on fait ensuite appel aux fonctions ap_getword ap_getword et aux fonctions apr_tables de apr afin de découper et stocker les différentes valeurs envoyées:

static int read_post(request_rec *r, apr_table_t **tab)
{
    const char *data;
    const char *key, *val, *type;
    int ret = OK;

    if(r->method_number != M_POST) {
        return ret;
    }

    // On verifie que le content type soit bien application/x-www-form-urlencoded
    type = apr_table_get(r->headers_in, "Content-Type");

    if(strcasecmp(type, DEFAULT_ENCTYPE) != 0) {
        return DECLINED;
    }

    if((ret = chunk_reader(r, &data)) != OK) {
       return ret;
    }

    // Creation ou RAZ de la apr_table_t
    if(*tab) {
        apr_table_clear(*tab);
    }
    else {
        *tab = apr_table_make(r->pool, 8);
    }

    // Tant qu'il y a des datas...
    while(*data && (val = ap_getword(r->pool, &data, '&'))) {
        key = ap_getword(r->pool, &val, '=');
        ap_unescape_url((char*)key);
        ap_unescape_url((char*)val);
        apr_table_merge(*tab, key, val);
    }

    return OK;
}

Récupérer la bonne valeur

Une fois qu’on a le tableau associatif, il nous faut une fonction pour récupérer la valeur voulue:

int get_value(request_rec *r, const char *name)
{
    apr_table_t *post_values = NULL;
    apr_array_header_t *arr;
    apr_table_entry_t *elt;

    int i, ret, value;

    if(OK != (ret = read_post(r, &post_values))) {
        return -1;
    }

    if(NULL == post_values) {
        return -1;
    }

    arr = apr_table_elts(post_values);
    elt = (apr_table_entry_t*)arr->elts;

    for(i = 0; i < arr->nelts; ++i) {
        if(strcmp(elt[i].key, name) == 0
        && LONG_MIN != (value = strtol(elt[i].val, NULL, 10))) {
            return value;
        }
    }

    return -1;
}

Revenons au code du module lui-même…

Il ne reste plus qu’à coder le handler de génération, et rajouter les structures nécessaires pour le module:

static int mod_randomint_method_handler (request_rec *r)
{
    int ret = OK, i, count = 8, new_count;
    unsigned int random_value;

    new_count = get_value(r, "count");

    if(new_count > 0 && new_count < MAX_COUNT) {
        count = new_count;
    }

    ap_set_content_type(r, "text/plain");

    int random_fd = open("/dev/urandom", O_RDONLY);
    for(i = 0; i < count; i ++) {
        read(random_fd, (void*)&random_value, sizeof(random_value));
        ap_rprintf(r, "%u\n", random_value);
    }
    close(random_fd);

    // fprintf(stderr, "Count: %d\n", count);
    // fflush(stderr);

    return ret;
}

static void mod_randomint_register_hooks (apr_pool_t *p)
{
    ap_hook_handler(mod_randomint_method_handler, NULL, NULL,APR_HOOK_LAST);
}

module AP_MODULE_DECLARE_DATA randomint_module =
{
    STANDARD20_MODULE_STUFF,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    mod_randomint_register_hooks,    /* callback for registering hooks */
}

Il ne reste plus qu’à compiler le code …:

apxs2 -c -a -i mod_randomint.c

… et le configurer dans httpd.conf:

LoadModule randomint_module /usr/lib/apache2/modules/mod_randomint.so

    <Location /randomint>
      SetHandler randomint-handler
    </Location>

Et après un redémarrage d’apache:

$ curl http://localhost/randomint
    4162010288
    1220025110
    2641785880

On pourra voir le résultat dans mes snippets et le télécharger dans mes projets directement.

Mise à jour:

J’ai oublié de filtrer par handler dans le code. On incluera donc dans mod_randomint_method_handler:

    if(strcmp(r->handler, "randomint-handler")) {
        return DECLINED;
    }

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');
    }

Firebug & xpath

Pour mon nouveau job, j’ai mis en place Selenium pour les tests fonctionnels sur le projet, et rapidement j’ai eu le besoin de tester une expression xpath pour l’un de mes tests, et je n’avais pas envie d’installer une application tierce rien que pour ça.

Après une rapide recherche, il suffit simplement d’utiliser la fonction $x(« … expression xpath… ») dans la console de firebug pour tester rapidement la validité d’une expression, et son résultat.

Plus de fonctions de la console firebug sont documentés sur cette page.

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/.