Le décor

Le programme sera donc codé en Java. Une classe JIrcRobot se chargera de se connecter au serveur IRC, et de communiquer avec lui. Une interface permettra de définir les méthodes à implémenter par les plugins. Elle sera originalement nommée Plugin. La classe JIrcRobot chargera les plugins disponibles, c'est à dire ceux contenus dans le dossier originalement nommé plugins et dont l'extension est .js, puisqu'ils contiennent du code JavaScript. Cela permettra aussi de pouvoir désactiver le chargement d'un plugin en modifiant simplement son extension. Une fois les plugins chargés, le programme attendra des messages du serveur. A chaque message reçu, il interrogera tout ses plugins pour savoir si l'un d'eux est concerné par le message. Si c'est le cas, le plugin sera alors exécuté et pourra alors réagir en envoyant des messages au serveur IRC.

Le test

Pour ceux qui préfèrent voir les choses fonctionner tout de suite, et voir comment ca marche, je vous propose de tester tout de suite le programme. Il vous faut récupérer tous les fichiers en annexe de ce programme : le fichier JIrcRobot.java ainsi que tous les fichiers en .js, qui seront à mettre dans un dossier nommé plugins. JIrcRobot.java sera à mettre au même niveau d'arborescence que le dossier plugins. Vous pouvez sinon juste prendre le fichier jircrobot.tar.gz et le décompresser.

Ouvrez ensuite le fichier JIrcRobot.java et éditez les paramètres de connexion, situés sous le commentaire "CONFIGURATION DE LA CONNEXION AU SERVEUR" pour donner un pseudonyme au robot, lui indiquer sur quel serveur il doit se connecter, et quels canaux il doit rejoindre. Sauvegardez les modifications et compilez le programme :

javac JIrcRobot.java

Vous pouvez maintenant l'exécuter :

java JIrcRobot

Le robot charge les plugins puis se connecte au serveur. En l'état actuel, le robot est capable de répondre aux PING du serveur, afin qu'il ne coupe pas la connexion, de rejoindre des canaux de discussion, de se présenter quand il rejoint un canal et souhaiter la bienvenue à ceux qui arrivent ensuite, de se couper si quelqu'un tape "!QUIT" et enfin de crier "COCORICO" si une phrase contient "france". Ces fonctionnalités sont toutes apportées par les plugins. Le programme Java en lui même ne sait rien faire de tout ca.

La classe JIrcRobot

Il est maintenant temps de comprendre comment tout cela fonctionne. Je passe rapidement sur les fonctionnalités liées au protocole IRC. Sans entrer dans le code, il faut juste savoir que la classe propose ces méthodes :

connect() : se connecte au serveur IRC
getNickName() : renvoie le pseudonyme du robot
joinChannels() : envoie les commandes au serveur IRC pour rejoindre des canaux de discussion
loadPlugins() : charge tous les plugins disponibles dans un dossier
readLine() : lis un message provenant du serveur, s'il y en a un d'arrivé
run() : boucle conservant la connexion avec le serveur, et exécutant les plugins si besoin
setStayConnected() : permet d'indiquer si l'on souhaite rester connecté au serveur ou non
write() : envoi un message au serveur IRC


Les méthodes importantes seront détaillées ensuite : loadPlugins et run.

Enfin la classe contient une interface privée, Plugin, qui permet de définir les méthodes que devront implémenter les plugins :

getPattern() : retournera l'expression régulière qui si elle est validée, indiquera que le plugin doit réagir au message arrivé
patternMatch() : cette méthode sera appelée si l'expression régulière du plugin match avec le message arrivé

Nos plugins devront donc implémenter ces deux méthodes.

Le programme Java

Après avoir vu les méthodes permettant de dialoguer avec le serveur, intéressons nous, dans le détail cette fois-ci, au code des deux principales. Avant cela, un rapide coup d'œil à la méthode main du programme :

public static void main(String[] args) {
    JIrcRobot jir = new JIrcRobot();
    jir.loadPlugins("plugins");
    if (jir.connect()) {
        jir.run();
    }
}

Rien de complexe : on créé un objet JIrcRobot, on charge les plugins, on connecte le robot au serveur IRC, et enfin on se met en attente de réception des messages avec la méthode run(). Il est maintenant temps de voir comment sont chargés les plugins, avec la méthode loadPlugins ;

public void loadPlugins(String folder) {
    ScriptEngine engine;
    Invocable inv;
    File dir = new File(folder);
    for (File file : dir.listFiles()) {
        if (file.isFile() && file.getName().toLowerCase().endsWith(".js")) {
            try {
                engine = new ScriptEngineManager().getEngineByName("JavaScript");
                engine.put("robot", this);
                engine.eval(new FileReader(file.getAbsolutePath()));
                inv = (Invocable) engine;
                this.plugins.add(inv.getInterface(Plugin.class));
                System.out.println(file.getAbsolutePath() + " loaded.");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Cette méthode va lister (listFiles) le dossier dont le nom lui est passé en paramètre (folder). Pour chaque fichier dont l'extension est .js (endsWith), elle créé un moteur de JavaScript (getEngineByName) et place dans l'environnement du script une variable nommée robot pointant sur l'objet JIrcRobot (put). Ensuite elle lit le contenu du fichier et stocke dans le tableau plugins une instanciation de l'interface Plugin provenant du fichier (getInterface).

Le chargement des plugins consiste donc à stocker des objets implémentant l'interface Plugin et dont le contenu provient des fichiers JavaScript.

Maintenant, la méthode run :

public void run() {
    String buffer;
    Matcher matcher;
    while (this.stayConnected) {
        buffer = this.readLine();
        if (!buffer.equals("")) {
            System.out.println(buffer);
            for (Plugin p : this.plugins) {
                matcher = Pattern.compile(p.getPattern()).matcher(buffer);
                if (matcher.find()) {
                    p.patternMatch(matcher);
                }
            }
        }
    }
}

Cette méthode contient une boucle qui attend un message du serveur IRC (readLine). Une fois qu'un message est reçu, elle boucle sur tous les plugins chargés. Pour chacun d'eux, elle teste si le pattern du plugin (p.getPattern()) match avec le message reçu (buffer). Si c'est le cas (find), la méthode patterMatch() du plugin est appellée et on lui passe en paramètre le matcher, ce qui permettra au plugin d'analyser les groupes capturés grâce à l'expression régulière. C'est assez pratique car le plugin reçoit ainsi seulement ce qui l'intéresse, et qu'il a lui-même spécifié dans la regex.

C'est ici que s'arrête le détail ! Nous avons un système qui stocke des plugins et un autre qui communique avec eux pour savoir s'il faut les solliciter (grâce à un système de regex ici, mais ca pourrait être autrement) et qui qui les exécute si besoin. Ce robot IRC est donc ainsi fait d'un noyau, disposant de peu de fonctionnalités, mais extensible à volonté.

Les plugins

Notre programme étant prêt, concentrons nous maintenant sur les plugins. Nous créerons un fichier par plugin

ping.js

Le serveur IRC envoie régulièrement un message PING auquel le client doit répondre. Si la réponse n'est pas envoyée dans un certain laps de temps, le serveur rompt la connexion. Ce plugin est donc vital. Le message est du type "PING :msg". Le client doit répondre "PONG :msg". msg est parfois le nom du serveur, ou parfois un numéro aléatoire. Pour un PING reçu, on récupèrera donc la partie droite, et on s'en servira pour envoyer le PONG. Voici le contenu de ping.js :

importClass(java.util.regex.Matcher);

function getPattern() {
    return 'PING\\s:(.*)';
}

function patternMatch(matcher) {
    robot.write("PONG :" + matcher.group(1));
}

On commence par importer la classe Java Matcher. On implémente ensuite les deux méthodes nécessaires de l'interface Plugin. Le regex renvoyé par getPattern contient un groupe qui capturera la partie à renvoyer par le PONG. La fonction patternMatch qui est appelée si le regex a matché envoie le PONG suivi du groupe capturé dans le PING. Pour cela elle utilise la variable robot représentant l'objet JIrcRobot pour envoyer un message au serveur.

endOfMOTD.js

Le Message Of The Day (MOTD) est un texte envoyé par le serveur au client lorsqu'il se connecte. Il contient souvent des informations sur le serveur. Une fois le MOTD envoyé, le serveur envoie un message indiquant la fin du MOTD du type : ":calvino.freenode.net 376 jircrobot :End of /MOTD command.". Il est important de réagir à ce message car certains serveurs n'autorisent pas le client à rejoindre des canaux tant que le MOTD n'a pas été envoyé. En attendant la réception de ce message pour rejoindre des canaux, on est sûr qu'on ne sera pas bloqué. Ce plugin réagira donc à ce message, et une fois reçu, appellera la méthode joinChannels de la classe JIrcRobot :

importClass(java.util.regex.Matcher);

function getPattern() {
    return ':.*\\s376\\s.*';
}

function patternMatch(matcher) {
    robot.joinChannels();
}

C'est assez simple ici encore. On donne la regex pour capturer la fin du MOTD et lorsque que le message est arrivé, on utilise la variable robot pour se connecter aux canaux.

quit.js

Ce plugin permettra de stopper le bot. Lorsqu'une personne tapera !QUIT sur un canal, le robot se stoppera. Biensûr dans le cadre d'un "vrai" bot, il faudrait s'assurer que la personne voulant couper le robot en a le droit, en se basant sur son nom d'hôte ou son pseudonyme. Le message envoyé par le serveur sera du type ":someone!~someident@some-host.com PRIVMSG #channel1 :!QUIT". Voici le contenu du plugin :

importClass(java.util.regex.Matcher);

function getPattern() {
    return ':.*\\sPRIVMSG\\s[^ ]+\\s:!QUIT';
}

function patternMatch(matcher) {
    robot.setStayConnected(false);
}

La regex ne capture rien ici car on ne gère pas de droits. Pour quelque chose de plus sécurisé, il faudrait capturer au moins le pseudonyme de la personne. La fonction appelée utilisera la méthode setStayConnected avec false en paramètre, ce qui aura pour effet de stopper la méthode run du robot.

join.js

Afin de rendre notre robot un minimum sociable, ce plugin aura pour tâche de le faire se présenter lorsqu'il rejoint un canal et de souhaiter la bienvenue aux nouveaux arrivants. Pour cela, il suffira de capturer les messages de type JOIN provenant du serveur, qui sont envoyés quand une personne rejoint un canal, ou quand le bot lui même le fait. Le message est du type ":nick!~ident@hostname.fr JOIN :#channel". Il suffit alors de comparer "nick" avec le propre pseudonyme du robot. S'il sont équivalents, c'est que c'est lui qui entre sur un canal, il doit donc se présenter. Si ce n'est pas le sien, alors il souhaite la bienvenue au nouvel arrivant. Il faudra donc capturer le pseudonyme et le nom du canal afin de s'en servir pour envoyer le message :

importClass(java.util.regex.Matcher);

function getPattern() {
    return ':([^!]+).*\\sJOIN\\s:([^ ]+)';
}

function patternMatch(matcher) {
    // Si c'est le robot qui JOIN
    if(matcher.group(1) == robot.getNickname()) {
        robot.write("PRIVMSG "+matcher.group(2)+" :Salut tout le monde, je suis "+matcher.group(1));
    }
    else { // si c'est un nouvel arrivant
        robot.write("PRIVMSG "+matcher.group(2)+" :Salut "+matcher.group(1));
    }
}

On voit donc la regex avec les deux captures (nick + canal). Ces informations sont ensuite utilisées pour le message envoyé.

patriote.js

Un plugin beaucoup moins utile, qui permettra simplement de montrer comment réagir à un texte particulier. Nous souhaitons ici que le bot montre qu'il est fier d'être français. A chaque fois que le mot France (peu importe la casse) sera écrit, il réagira. Le message reçu sera du type ":someone!~someident@some-host.com PRIVMSG #channel :Vive la France". Voyons le code du plugin :

importClass(java.util.regex.Matcher);

function getPattern() {
    return '(?i):[^!]+.*\\sPRIVMSG\\s([^ ]+)\\s:.*france.*';
}

function patternMatch(matcher) {
    robot.write("PRIVMSG "+matcher.group(1)+" :COCORICO !!!");
}

La regex indique que la recherche sera insensible à la casse. On capture simplement le nom du canal d'où le texte est envoyé pour pouvoir répondre.


Voilà pour quelques exemples de plugins. Le but ici est de montrer que presque tout est possible, mais que surtout ce genre de programme, modulable, auquel on peut ajouter des fonctionnalités, est facilement réalisable. Pour les plus courageux qui auront lu jusque ici (j'espère que number80 en fait partie !), n'hésitez pas à poser des questions dans les commentaires. Il n'est pas facile de synthétiser tout cela en un seul billet, il y a donc probablement des passages obscurs !


Fabien (eponyme)