P
'
t
i
t
e
C
h
a
t
t
e
 
spacer~ VIRGINITY LIKE BUBBLE, ONE PRICK, ALL GONE Articles | Connexion
 
~Un client IRC en Python

Précédent  
  Python  
  Suivant
 Présentation

Sur un système informatique récent, l'utilisateur peut opter pour de nombreuses solutions de dialogue. L'une d'entre elle, l'une des plus anciennes, se nomme IRC. Nous allons voir dans ces colonnes comment développer une application de discussion IRC.
 Sommaire


 Le protocole client

L'Internet Relay Chat désigne le moyen de communication favori de nombreuses personnes, notamment des développeurs. Basé sur le protocole textuel du même non, ce système possède l'avantage d'être bien documenté et surtout largement supporté. Le choix du développement d'un tel outil en Python découle de certaines considérations évidentes. Étant donné le caractère universel du protocole IRC, l'usage d'un langage multi plates-formes facilitera la distribution du produit. En outre, Python se révèle particulièrement à l'aise dans le traitement de chaînes de caractères, opération prédominante au cour de tels clients ainsi que vous le constaterez lors de la découverte de la RFC. En particulier, la lecture du code source de ce projet vous permettra de revenir sur toutes les facettes de Python étudiées depuis le début de cette série.

C'est en 1993 que fut publiée la RFC 1459 décrivant le protocole IRC dans son intégralité. Celle-ci fut suivie l'année d'après par la spécification CTCP (Client To Client Protocol) qui introduit de nouvelles commandes telle que le DCC (Direct Client Connexion, notamment employé pour l'échange de fichiers). Enfin, en l'an 2000, quatre RFC, numérotées de 2810 à 2813, apparurent pour remplacer l'originelle, chacune d'entre elles se focalisant sur un aspect bien précis d'IRC. Notre attention se portera principalement sur la RFC 2812 qui décrit le protocole client uniquement.


 Des commandes et des réponses

A l'instar de ses homologues POP3 et SMTP destinés à la gestion du courrier électronique, la communication entre un client IRC et son serveur repose sur la transmission de simples lignes de textes. La RFC sur laquelle nous nous penchons décrit des règles bien précises à ce sujet. Nous devons donc étudier et respecter ces dernières à la lettre pour que nos requêtes soient valides et pouvoir utiliser correctement ce que le serveur nous transmet. Chaque commande émise par le client prend la forme d'un mot-clé en lettres capitales suivi d'éventuels paramètres, est terminée par la paire de caractères \r\n, et ne doit pas excéder 512 octets. Si notre chaîne de caractère contient une erreur, elle s'avère simplement ignorée. Dans le cas inverse, la cible va la prendre en compte et y répondre. La réponse possède un préfixe constitué du caractère deux-points, parfois suivi du nom du serveur. La présence de ce dernier est conditionnée d'une part par le type d'implémentation et d'autre part par la commande émise. Nous considérerons dans ces lignes que le serveur insère son nom dans toutes les requêtes le supportant. Notons enfin que ces messages peuvent comporter un numéro d'identification faisant partie de l'intervalle 001 à 099 pour les tentatives de connexion, et de la plage 200 à 399 pour les commandes.

Illustrons cela avec la commande NICK permettant de changer de pseudonyme. L'utilisateur Mike désire se renommer en John. Deux cas de figures sont possibles. Dans le premier, un autre John est déjà connecté au serveur. Comme il ne peut y avoir d'homonyme sur IRC, la commande ne sera pas exécutée. Les lignes de textes envoyées sont, du client vers le serveur :

NICK John
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Et du serveur vers le client (réponse) :

:<nom_du_serveur> 433
       
      
JextCopier dans Jext
La réponse numérique 433 indique l'erreur rencontrer. Mike conserve son nom d'origine.

Dans le deuxième cas de figures, il n'y a pas d'autre John, le serveur exécute la commande et émet une ou plusieurs réponses à tous les clients concernés. Les lignes de texte envoyées seront les même depuis le client, seule la réponse du serveur change :

:Mike!<identifiant_connexion> NICK John
       
      
JextCopier dans Jext
La ligne précédente ne comportait pas de numéro. Néanmoins, il faut savoir qu'afin de rendre compte de l'exécution d'une commande, le serveur peut utiliser les messages numériques. Ceci ce produit notamment lorsqu'il envoi plusieurs lignes représentant une liste de données et qu'il désire en marquer la fin.

Par exemple, pour indiquer au client de Mike la fin de l'émission de la liste des usagers d'un canal de discussion, le serveur émet une réponse semblable à la suivante :

:<nom_du_serveur> 366 Mike <nom_du_canal> :End of NAMES list
       
      
JextCopier dans Jext
La réponse transmise est clairement identifiée par le numéro 366. Nous garderons à l'esprit que pour toute commande, le serveur peut envoyer une réponse numérique correspondant à une erreur. Les numéros sont expliqués dans la RFC 2813.

Détaillons maintenant le fonctionnement du protocole pas à pas.


 L'identification

La connexion avec le serveur a été établie sur un port précis ( les serveurs IRC proposent, en règle générale les ports allant de 6666 à 7000). La documentation stipule qu'à ce stade, le client doit s'identifier auprès du serveur pour que le dialogue puisse commencer. Ceci s'effectue évidement par l'intermédiaire de lignes de textes dont la forme générale correspondra à celle décrite plus haut. Cette première étape est menée à bien par l'envoi des requêtes NICK puis USER.

Voici un exemple d'ouverture de session :

NICK Mike
USER Michael 0 * :Michael Mondarse
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
La première ligne indique le pseudonyme de l'utilisateur, tandis que la seconde l'identifie de manière formelle auprès du serveur. Les paramètres utilisés correspondent respectivement au nom d'utilisateur, au mode, à un paramètre inutile et au nom réel.

A l'issue de ces deux commandes le serveur renvoie au client un message de bienvenue validant la session, le système de numérotation expliqué plus haut nous permet d'identifier ces réponses. Après identification, un client, commandé par son utilisateur, effectue diverses opérations. Une commande sera envoyée pour chaque action. Nous allons nous intéresser ici au cas d'un client IRC qui après connexion rejoint un canal de discussion. Ensuite, l'utilisateur va devenir opérateur, ce qui nous donnera la possibilité d'intervenir sur les modes, les utilisateurs et les canaux. Cela nous permettra d'expliquer le fonctionnement des principales commandes décrites dans le protocole IRC.


 Joindre un canal

Dans notre exemple, l'utilisateur a décidé de rejoindre le canal #progx. Le client va devoir une nouvelle fois former une commande et la transmettre à son serveur. Cette dernière sera constituée du mot-clef JOIN suivi du nom du canal. La réponse du serveur dans ce cas peut paraître moins évidente. Le listing ci après la représente.

:Mike ! <identifiant_connexion> JOIN :#progx
:<nom_du_serveur> 332 Mike #progx : un bon canal
:<nom_du_serveur> 353 Mike = #progx : Mike Julie @Gfx @Dams
:<nom_du_serveur> 366 Mike #progx :End of NAMES list
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Le serveur confirme tout d'abord, avec la première ligne, la réception de la commande et en annonce l'exécution. Il donne ensuite, avec les réponses 332 et 353 le sujet du canal et la liste des personnes présentes. La liste des utilisateurs peut apparaître sur plusieurs lignes. Dans ce cas, le serveur émettra plusieurs réponses de code 353. On peut repérer la fin de la liste grâce au message numéroté 366. Ce type de réponse apparaît avec l'utilisation d'autres commandes, telle LIST pour recevoir la liste des canaux.


 Communiquer

Maintenant que nous sommes en présence d'autres membres de la communauté IRC, nous allons pouvoir dialoguer avec eux. Ceci peut ce faire soit directement d'un utilisateur à un autre soit par l'intermédiaire d'un canal. Notre client va donc devoir envoyer une requête contenant notre texte au serveur qui l'enverra aux cibles, canaux ou utilisateurs :

PRIVMSG <cibles> <texte>
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
L'ensemble des cibles séparées par une virgule et le texte à envoyer constituent les paramètres de la commande PRIVMSG. Le serveur envoie ensuite un message similaire aux cibles :

:<nom_du_serveur> PRIVMSG <nom_du_canal_source|nom_de_l'utilisateur_source> <texte>
       
      
JextCopier dans Jext
On rencontre après le préfixe et le mot clé, l'expéditeur du message et le texte.


 Les modes

Sur IRC certaines personnes sont principalement connues comme étant les seul capable de déconnecter un utilisateur d'un salon de discussion. On les nomme opérateurs. Mais ils sont aussi habilités à changer le mode des participants ainsi que celui de leurs canaux. Pour ces derniers les principaux sont o ,v, m et b correspondant respectivement à des modifications au niveau : du privilège d'opérateur, de la possibilité de s'exprimer sur un canal modéré, de la modération d'un salon et du bannissement d'une personne. Pour ce qui est des utilisateurs on relèvera les modes i et s marquant la cible comme invisible ou comme autorisée à recevoir les notifications du serveur.

Appliquons cela à notre exemple. Un des opérateurs du canal #progx a eût l'amabilité de changer le mode du salon pour que Mike obtienne les même droits que son bienfaiteur. Ainsi il peut changer son mode et celui du salon. Dans le premier cas son client devra envoyer une commande du type :

MODE Mike (+|-) (i|s)
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
La requête se forme assez simplement. Elle débute par le mot-clef MODE, immédiatement suivis du pseudonyme de la cible. Vient ensuite un caractère spécifiant que l'on attribut (+) ou que l'on retire (-) une qualité à l'utilisateur. Enfin une lettre exprime cette qualité (ici i ou s). Dans le cas ou Mike souhaite modifier le mode du canal la ligne envoyée aura la forme suivante :

MODE #progx (+|-)(o|m|b|v)
       
      
JextCopier dans Jext

 Ping ? Pong !

Pendant que Mike discutera, son serveur va régulièrement s'assurer que son client est toujours actif en l'interrogeant avec des requêtes PING. Pour pouvoir conserver la connexion le client doit répondre par un PONG. Ceci est essentiel, si le client ne dispose pas de cette fonction il ne pourra pas rester connecté plus de quelques minutes.

PING :<nom_du_serveur>
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Dès que le client reçoit cette ligne il doit répondre par :

PONG :<identifiant_connexion>
       
      
JextCopier dans Jext
La connexion est maintenue jusqu'au prochain PING.


 La fin du dialogue

Tout a une fin, et il arrivera certainement que Mike souhaite quitter son salon, voir se déconnecter. Pour cela son client devra émettre quelques ultimes commandes. Tout d'abord pour quitter un canal le logiciel doit former une requête PART. Cette opération se révèle aisée puisqu'elle ne diffère de la commande JOIN que par son mot-clé. Enfin vient le moment de la déconnexion :

QUIT :<message_d'adieu>
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Il n'y a aucune réponse du serveur. Nous ne pouvons évidemment pas détailler ici l'ensemble du protocole IRC. Mais nous en avons examiné les grandes lignes. Ceci va nous permettre une meilleure approche de l'implémentation d'un client de ce type.


 Implémentation

L'implémentation Python nous allons étudier se compose de 7 modules Python. Parmi ceux-ci, 2 retiendront notre attention : IrcServerConnection.py et MainIrcClientFrame.py. Le premier accueille le moteur de gestion du protocole IRC et le second l'interface graphique reposant sur ce moteur.

L'architecture de ce dernier se révèle assez sobre : nous exécutons une boucle "infinie" (dans la méthode __principalLoop) au sein d'un thread (créé par la méthode threadPrincipalLoop) recueillant les données depuis un socket (self.connectionIrc). Hormis ces 3 méthodes, toutes les autres servent à émettre des commandes IRC au serveur. Nous possédons ainsi la méthode privMsg() qui autorise l'émission d'un message à une ou plusieurs cibles. Vous pourrez vous référer au listing 1 pour la découvrir. L'invocation de send() réalise simplement l'émission de la chaîne passée en paramètre par l'entremise du socket connectionIrc.


 Traitement des messages

La partie la plus intéressante de notre exemple réside dans __principalLoop et sa boucle. Au sein de celle-ci, nous allons lire des blocs de 512 octets de données depuis le serveur, les joindre sous forme de lignes de texte (terminées par un retour chariot donc). Il s'agit de la partie la plus sensible de notre application. Voici comment notre moteur recueille les lignes de texte émises par le serveur :

tuple = self.getMsgFromBuffer(buffer)
liste = tuple[0]
buffer = tuple[1]
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
La méthode getMsgFromBuffer() prend en paramètre un tampon, contenant ce que le serveur nous transmet, et retourne un tuple. Dans celui-ci, le premier élément désigne une liste de lignes lues depuis le socket, tandis que le second représente ce qui n'a pu être considéré comme une ligne complète. Ces "restes" sont placés dans le tampon pour subir une nouvelle analyse. A chaque passage de la boucle, nous obtenons les lignes complètes envoyées par le serveur et ajoutons au tampon 512 nouveaux octets lus depuis le socket.

L'étape suivante consiste à reconnaître le type de message pour chaque ligne émise par le serveur. A cet effet, nous utilisons les expressions régulières. Le moteur s'articule autour de l'objet regexIRC, l'expression régulière principale. Cette dernière a pour caractéristique de pouvoir reconnaître un PING ou toute commande IRC valide. Nous la retrouvons en intégralité dans le listing 2. Comme vous pouvez le constater, il s'agit d'une expression à l'allure particulièrement alambiquée. Toutefois, en vous appuyant sur la RFC, vous serez en mesure de la décoder très vite. La ligne de code suivante récupère les informations capturées par l'expression si la ligne n'est pas un PING :

dump, nick, id, command, source, arg, text = match.groups(5)
       
      
JextCopier dans Jext
La variable dump contient un groupe inutile dans ce cas. Les autres variables désignent respectivement le pseudonyme de notre utilisateur, son identificateur, la commande IRC exécutée, sa source, ses arguments et enfin une chaîne de caractères, optionnelle. Par exemple lorsque nous recevons un message sur un canal, nos variables prendront ces valeurs :

Ligne reçue : Gfx_!~rguy@modemcable021.162-203-24.mtl.mc.videotron.ca PRIVMSG dams :Bonjour
nick : Gfx_
id : ~rguy@...videotron.ca
command : PRIVMSG
source : dams
arg : [rien]
text : Bonjour
       
      
JextCopier dans Jext
Si l'application de l'expression échoue, nous pouvons en conclure que la ligne analysée est en fait une réponse numérique. Dans ce cas, une nouvelle expression s'applique :

:(\S+)\s+(\S+)\s+(\S+)\s+(.*)
       
      
JextCopier dans Jext
Les groupes capturent le nom du serveur, le numéro de réponse, notre pseudonyme et des données. Pour chaque code de réponse différent, le traitement de ces dernières va différer. En lisant le code du moteur, vous remarquerez que de nombreuses autres expressions régulières, très différentes, sont appliquées sur les données.

Ainsi, grâce à la souplesse de Python et la puissance des expressions régulières, nous pouvons réaliser un moteur de client IRC sans trop d'efforts. Le moteur lui-même ne se révèle pas très compliqué, mais particulièrement long, eu égard aux nombreuses commandes et réponses IRC existantes. Pour prolonger l'expérience, nous vous incitons à découvrir le protocole CTCP DCC décrit dans le fichier ctcpspec.html sur le CD, et permettant l'échange de fichiers. Vous pourrez vous reporter au code source de ProgXRC pour un exemple d'implémentation, en vous référant au module MainIrcClientFrame.py.

def privMsg(self, targetList, message):
  commande = "PRIVMSG "
  for target in targetList[:-1]:
    commande = commande + target + ","
  commande = commande + targetList[-1] + " :" + message + "\r\n"
  self.send(commande)
       
      
JextCopier dans Jext
(?m)^(PING\s+:.*)|^:([^!:]+)!(\S+)\s+([A-Za-z]+)\s+(#?[^: \t]+)?\s*([^:]+)?:?([^\r]*)
       
      
JextCopier dans Jext

 Téléchargements

Le code source complet de ProgXRC se trouve dans la rubrique téléchargements du site.



par Romain Guy
romain.guy@jext.org
http://www.jext.org
Dernière mise à jour : 14/10/2006


Précédent  
  Python  
  Suivant

 
#ProgX©2005 Mathieu GINOD - Romain GUY - Erik LOUISE