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.

Je suis en train de développer sygit, une interface web light pour naviguer dans un dépôt git. J’ai alors décidé de mettre au clair mes dépots et de monter ma propre architecture d’hosting git, sous debian.

Emplacement des dépôts

Les dépôts vont être accessible via ssh (lecture+écriture) et via git-daemon (par dessus d’inetd, en lecture seule). On décide de créer un groupe « git-users » pour ceux qui auront accès aux dépots. On édite donc /etc/group:

git-users:x:199:mycroft,patrick

Le dépôt sera physiquement dans /git. On le crée (et on n’oublie pas le –bare !):

# mkdir -p /git/newproject
# cd !-1:2
# git --bare init
Initialized empty Git repository in /git/newproject/

On pourra également réutiliser un projet déjà existant dont on créerait un dépôt bare:

# cd /git
# git clone --bare git://github.com/mycroft/sygit.git
Cloning into bare repository sygit.git...
remote: Counting objects: 48, done.
remote: Compressing objects: 100% (47/47), done.
remote: Total 48 (delta 21), reused 0 (delta 0)
Receiving objects: 100% (48/48), 8.77 KiB, done.
Resolving deltas: 100% (21/21), done.

Pour ces projets, il faudra changer la configuration et leur indiquer qu’il s’agit d’un dépôt partagé sur la base du groupe. Pour cela, il faut modifier la propriété core.sharedRepository:

# cd sygit.git
# git repo-config core.sharedRepository group

On n’a plus qu’à fixer les permissions manuellement:

# chgrp -R git-users /git/sygit.git
# find /git/sygit.git -type d -exec chmod g+ws {} \;

Mise en place de git-daemon

On utilise inetd pour partager les dépôts. On édite donc /etc/inetd.conf

git stream tcp nowait nobody /usr/bin/git git daemon --inetd --verbose --export-all --base-path=/git /git

… et on redémarre le daemon:

/etc/init.d/openbsd-inetd restart

Testons !

Sur ma machine de travail, on clone le dépôt en read-only:

$ git clone git://mkz.me/sygit.git
Initialized empty Git repository in /tmp/testgit/sygit/.git/
remote: Counting objects: 48, done.
remote: Compressing objects: 100% (26/26), done.
remote: Total 48 (delta 21), reused 48 (delta 21)
Receiving objects: 100% (48/48), 8.77 KiB, done.
Resolving deltas: 100% (21/21), done.

$ cd sygit
$ git remote -v
origin  git://mkz.me/sygit.git (fetch)
origin  git://mkz.me/sygit.git (push)

Pour le moment, le seul remote configuré est le serveur git. Celui là étant en read-only, il va faloir le modifier pour pouvoir push:

$ git remote rm origin
$ git remote add origin mycroft@mkz.me:/git/sygit.git
$ git remote -v
origin  mycroft@mkz.me:/git/sygit.git (fetch)
origin  mycroft@mkz.me:/git/sygit.git (push)

Il ne reste plus qu’à modifier un fichier, le commiter et le pusher:

$ vim README
$ git add README
$ git commit -m "Update README"
[master 2d98128] Update README
 1 files changed, 6 insertions(+), 0 deletions(-)
$ git push origin master
Counting objects: 5, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 375 bytes, done.
Total 3 (delta 2), reused 0 (delta 0)
To mycroft@mkz.me:/git/sygit.git
   826d9b0..2d98128  master -> master

Et voilà !

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

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

Il est de plus en plus courant d’avoir à faire à des fichiers mkv (Matroska). L’avantage de ces fichiers, c’est qu’il peuvent être composés de plusieurs pistes videos/audios/sous titres, et donc de permettre des vidéos multilingues. Le point négatif, est que les lecteurs de divx de salon ou bien encore la freebox ne les gèrent pas complètement (certains codecs manquant). Il est donc indispensable de convertir ces videos en divx/xvid avec en plus un bon codec sonore.

Pour cela, je vais faire appel aux packages mkvtoolnix, mplayer et mencoder sous gnu/linux.

Description des fichiers mkv

Les mkv sont capables d’être composés de flux vidéos et sonores encodés par différents types de codecs. Une première étape est de déterminer à quels codecs l’on a à faire. La commande mkvinfo permet cela:

$ mkvinfo "Morning Musume - Pepper Keibu.mkv" | grep -E "Codec ID: (V_|A_)"
|  + Codec ID: V_MPEG4/ISO/AVC
|  + Codec ID: A_AAC

Ce fichier, par exemple, contient une vidéo en mpeg4 et la piste audio est de l’AAC.

$ mkvinfo MyMovie.mkv | grep -E "(Codec ID: (V_|A_|S_)|Language)"
|  + Codec ID: V_MS/VFW/FOURCC
|  + Language: und
|  + Codec ID: A_AAC/MPEG2/LC/SBR
|  + Language: jpn
|  + Codec ID: A_AAC/MPEG2/LC/SBR
|  + Language: eng
|  + Codec ID: S_TEXT/ASS
|  + Language: eng

Ce fichier ci, par contre, contient une piste vidéo (le codec est préfixé par V_), deux pistes audios (une japonaise et une anglais), et une piste de sous titres anglaise. La liste complète des codecs supportés par Matroska est disponible sur cette page.

Lecture d’un mkv avec mplayer

Il est facilement possible de lire avec mplayer un tel fichier, en utilisant les bonnes options pour avoir la bonne piste audio/vidéo/sous-titres.

Par exemple, si l’on reprend le fichier précédent et que l’on veuille lire la version originale japonaise sous titrée anglais:

mplayer -alang jpn -slang 0 MyMovie.mkv

(ou encore: « mplayer -vid 0 -aid 0 -sid 0 MyMovie.mkv » pour sélectionner manuellement les pistes.)

mplayer donne de toutes façons plus d’informations au démarrage de la lecture sur les pistes:

Playing MyMovie.mkv.
[mkv] Track ID 1: video (V_MS/VFW/FOURCC) "MyMovie", -vid 0
[mkv] Track ID 2: audio (A_AAC/MPEG2/LC/SBR) "Dolby 5.1 Japanese", -aid 0, -alang jpn
[mkv] Track ID 3: audio (A_AAC/MPEG2/LC/SBR) "Dolby 5.1 English", -aid 1, -alang eng
[mkv] Track ID 4: subtitles (S_TEXT/ASS), -sid 0, -slang eng
[mkv] Will play video track 1.

Conversion d’un fichier mkv en fichier avi

Quand on a de la chance, le fichier mkv contient un flux vidéo xvid. Dans ce cas là, on peut facilement faire un fichier AVI en utilisant le flux vidéo existant. Pour la piste audio, on opte pour une simple conversion en PCM (passe partout).

La commande est:

mencoder nom_du_fichier.mkv -ovc copy -oac pcm -o nom_du_fichier.avi

Le paramètre -ovc donne le codec avec lequel on va encoder le flux video. Ici, `copy’ signifie qu’on ne réencode pas, on copie juste le flux. Le paramètre -oac permet de donner le codec audio utilisé. Ici, pcm.

Quand on n’a pas de chance, il faut réencoder le flux vidéo en xvid et le flux sonore en mp3. Et en prime, on veut vouloir sélectionner la piste audio (jpn), et inclure les sous titres.

Alons-y:

mencoder -vid 0 -alang jpn -slang eng \
    -oac mp3lame -lameopts cbr=128 \
    -ovc xvid -xvidencopts bitrate=900 \
    nom_du_fichier.mkv -o nom_du_fichier_final.avi

Et voilà !
On jouera avec les options d’encodage selon nos besoins (man mencoder est votre ami à partir de maintenant) pour avoir des fichiers plus petits ou une meilleure qualité.