Parlons XMPP - épisode 7 - cas pratiques: SleekXMPP et SàT

(pour lire les épisodes précédents, suivez l'étiquette correspondante)

Comme cela a été demandé plusieurs fois, nous allons pour cet article faire un petit cas pratique avec deux bots XMPP.

SleekXMPP

c'est SleekXMPP que nous allons tester, que je n'avais jamais utilisé avant cet article. Il s'agit d'une bibliothèque Python qui se veut simple, avec peu de dépendances, et gérant tout avec des greffons. Vous pouvez aussi utiliser le fork (amical) fait pour Poezio, Slixmpp qui a réécrit le cœur de la bibliothèque pour tout gérer en asynchrone (sans thread) via le récent asyncio (aussi il faut une version de Python >= à 3.4, alors que SleekXMPP fonctionne avec Python 2 et 3).

Sans entrer trop dans les détails, que vous trouverez sur le site officiel avec plusieurs tutoriels (http://sleekxmpp.com/), faisons un simple bot qui répond à des commandes ad-hoc (aucun lien avec Tintin, voir plutôt l'épisode 6):

Ce script est une adaptation de l'exemple donné dans le dépôt officiel (voir sa licence). Des explications sont données après le script, et il devrait fonctionner avec Python 2.6 ou supérieur ou 3.1 ou supérieur.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sleekxmpp
from sleekxmpp import jid

JID="mon_super_bot@mon_super_server.tld"
PWD="mon_super_mot_de_passe"
ADMIN=jid.JID("mon_admin@mon_super_server.tld")
CMD_ENVOI = u'envoi message'
CMD_DECO = u'déconnexion'
CMD_ADMIN = u'gros bouton rouge'


class MonSuperBot(sleekxmpp.ClientXMPP):

    def __init__(self):
        sleekxmpp.ClientXMPP.__init__(self, JID, PWD)
        self.add_event_handler("session_start", self.session_start)
        # note 1
        self.register_plugin('xep_0030')
        self.register_plugin('xep_0004')
        self.register_plugin('xep_0050')
        self.register_plugin('xep_0199', {'keepalive': True, 'frequency':15})

    def session_start(self, event):
        # note 2
        self.send_presence()
        self.get_roster()
        self['xep_0050'].add_command(node='monbot',
                                     name=u'Commandes de monbot',
                                     handler=self._handle_commands)

    def _handle_commands(self, iq, session):
        form = self['xep_0004'].makeForm('form', 'bot')
        form['instructions'] = u'Choisissez une commande'
        options = [CMD_ENVOI, CMD_DECO]

        # note 3
        if session['from'].bare == ADMIN:
            options.append(CMD_ADMIN)

        form.addField(var='command',
                      label=u'commande',
                      ftype='list-single',
                      required=True,
                      options=options
                      )

        session['payload'] = form
        session['next'] = self._handle_command_complete
        session['has_next'] = False

        return session

    def _handle_command_complete(self, payload, session):
        form = payload
        command = form['values']['command']

        if command == CMD_ENVOI:
            self.send_message(mto=session['from'],
                              mbody="Message important !",
                              mtype='headline')
        elif command == CMD_DECO:
            self.disconnect(wait=True) # note 4
        elif command == CMD_ADMIN:
            # note 5
            if session['from'].bare == ADMIN:
                session['notes'] = [('warning', 'BOOM !')]
            else:
                raise ValueError("ce n'est pas un admin !")
        return session


if __name__ == '__main__':
    xmpp = MonSuperBot()
    if xmpp.connect(): # note 6
        print(r"\\o/")
        xmpp.process(block=True)
    else:
        print(":(")

Explications

Après avoir importé sleekxmpp, nous utilisons quelques constantes (n'oubliez pas bien sûr de remplacer JID et PWD par des valeurs adaptées) pour simplifier, les exemples officiels permettent de manière plus élégante de préciser jid et mot de passe en ligne de commande.

Notre bot se comportera comme un client, nous héritons donc de sleekxmpp.ClientXMPP.

au niveau de la note 1 nous implémentons les greffons qui gèrent les XEPs qui nous intéressent :

  • disco (XEP-0030) pour annoncer ad-hoc, comme expliqué dans l'épisode 3
  • les formulaires de données (x-data, XEP-0004) et les commandes « ad-hoc » (commands, XEP-0050), expliquées dans l'épisode 6
  • la XEP-0199 permet d'envoyer et de répondre à des « pings », afin de s'assurer que la connexion et les logiciels sont toujours actifs.

À la note 2 nous envoyons notre présence et demandons la liste de contacts (« roster »), ce qui est requis par les RFCs, certains serveurs pouvant refuser de répondre si ce n'est pas fait.
Ensuite nous utilisons le greffon des commandes « ad-hoc » pour ajouter le point d'entrée à nos commandes.

Ce point d'entrée appellera la méthode « _handle_commands », et nous utilisons un formulaire (comme expliqué dans la XEP-0004) pour spécifier nos commandes. La liste « options » nous sert à indiquer nos commandes dans l'ordre voulu.

Au niveau de la note 3, nous profitons de l’identification forte de XMPP pour vérifier très simplement si le demandeur utilise l'adresse (jid) de notre compte privilégié, que nous avons indiqué dans la constante « ADMIN ». Nous aurons ainsi un jeu de commandes différent si nous utilisons le compte privilégié ou un autre.
Évidemment si vous voulez profiter de l'authentification forte, il faut vous assurer que tout est en ordre au niveau des certificats et du chiffrement (ce que fait a priori SleekXMPP de base, consultez la documentation pour plus d'informations).

L'ajout de commande se fait ici par une liste à choix unique (« list-single »), mais vous pouvez demander d'autres choses comme des mots de passe ou des jids, tout est expliqué dans la XEP-0004.

Quelques précisions sur ad-hoc : à chaque « page » vous pouvez effectuer une action:

  • cancel pour annuler
  • prev pour revenir en arrière
  • execute pour lancer une commande ou continuer la session
  • next pour passer à la page suivante
  • complete pour finir la sesssion

Ces actions sont gérées par sleekXMPP à travers le dictionnaire « session », la meilleure documentation que j'ai trouvée sur le sujet est le code lui-même : dans le fichier « sleekxmpp/plugins/xep_0050/adhoc.py » présent dans les sources.

Ad-hoc permet également d'indiquer un message, une « note », pour donner l'état de votre commande, elle peut avoir un type « info », « warn » (warning: attention) ou « error » (erreur).

Dans le code nous indiquons que nous avons notre dernière « page » par « session['has_next'] = False », et qu'il faut traiter la réponse dans « _handle_command_complete ». Oui c'est un peu contre-intuitif d’avoir à la fois has_next à False, et un next, mais ce dernier correspond à la réponse et non à une nouvelle page.

Passons à ces réponses. Nous regardons ce qui est dans le formulaire pour savoir quoi faire. Dans le premier cas nous envoyons un message manchette ou « headline » (voir l'épisode 2)

Dans le deuxième cas (au niveau de la note 4) on fait une déconnexion. Alors j'ai eu un problème ici avec SleekXMPP, que j'utilise pour la première fois comme indiqué plus haut : il faudrait pour faire propre terminer la session ad-hoc avant de déconnecter, mais je n'ai pas trouvé de moyen simple de le faire, vu que la déconnexion est immédiate et qu'il faut retourner le dictionnaire session pour terminer la séquence. Peut-être que je suis passé à côté de quelque chose, n'hésitez pas préciser en commentaire si vous avez une astuce.

Passons à la note 5: je refais une vérification du jid avant de lancer la commande privilégiée. La raison est simple: le formulaire est envoyé du client au bot par le serveur sans être vérifié par ce dernier, c'est au bot à le faire. Il est interdit d'ajouter une nouvelle commande, mais comme je doute que SleekXMPP conserve le formulaire d’origine pour vérifier qu'il n'y a rien de nouveau, un attaquant pourrait ajouter la commande privilégiée sans être admin. Cette vérification permet de l'éviter.

Pour faire propre il faudrait envoyer une réponse XMPP précisant que l'action est interdite, mais pour faire simple je me suis contenté d'une exception ici.

Enfin, au niveau de la note 6 on se connecte. Pour un serveur local de test, si vous n'avez pas de serveur DNS configuré comme il faut, vous pouvez spécifier l'adresse ip ou le nom de domaine local en dur, ainsi que le port (par exemple « self.connect(('localhost', 5222)) ». Si votre certificat n'est pas valide, vous pouvez aussi utiliser « use_tls=False » (pour les tests uniquement !). La documentation de SleekXMPP vous indiquera plus clairement comment gérer correctement les certificats.

Enfin, un autre problème que j'ai eu avec SleekXMPP: il ne semble pas possible avec ma version (1.3.1) d'indiquer de ne pas utiliser de commande « exécuter » ou de spécifier son équivalence (ce qui est pourtant possible avec la XEP-0050). Il faudrait qu'elle soit absente ou équivalent à « compléter ». Vous allez ainsi avoir un bouton qui sera inutile, et provoquera même une erreur. Là encore si quelqu'un plus habitué à la bibliothèque peut indiquer en commentaire comment corriger cela. Utilisez donc le bouton « compléter » (ou « finir ») et non « exécuter » de votre client.

Voilà la capture de notre bot piloté par Gajim:

bot piloté par Gajim

Salut à Toi

Pour indiquer les ajouts faits au code, nous utilisons un bot dénommé « Sabot ». Nous gérons les sources avec Mercurial (sur https://repos.goffi.org/). Celui-ci permet d'exécuter une commande via des crochets (« hooks »), dont un qui est lancé à chaque série de commits: incoming. Nous avons donc ceci dans notre hgrc:

[hooks]
incoming.sabot = /chemin/vers/hg_sabot_hook.sh

Pour le bot, nous profitons du côté multi-interfaces de SàT: SàT a une architecture qui permet pour un même client (et donc les mêmes profils, comptes, etc) d'avoir différentes interfaces (console, ligne de commande, bureau, web, etc).

Nous avons donc créé le compte comme un compte normal grâce à Primitivus, l'interface console (nous sommes sur un serveur sans interface graphique disponible). On peut joindre le salon désiré (ici sat@chat.jabberfr.org), et le mêttre en favori (grâce à la XEP-0048), ainsi que gérer l'entrée automatique (« autojoin »). Il ne reste plus qu'à envoyer le message quand le script hg_sabot_hook.sh est appelé, ce que nous faisons à l'aide de « jp », l'interface en ligne de commande:

#!/bin/sh

#on s'assure d'abord que D-Bus est lancé
eval `/chemin/vers/dbus-launch.sh`

REPOS_BASE="http://repos.goffi.org/sat"
PROJECT_FULL="Salut à Toi"
REPOS="$REPOS_BASE/rev/$HG_NODE"
MUC="sat@chat.jabberfr.org"

hg log -r $HG_NODE --template "Commit from {author|person} on $PROJECT_FULL\\n{desc}\\n$REPOS" | jp message -cp sabot $MUC

(le vrai script est un tout petit peu plus long puisqu'il gère aussi les noms des autres dépôts).

L'option « -c » indique de se connecter si ça n'est pas le cas, et « -p sabot » précise qu'il faut utiliser le profil « sabot »

Le script dbus-launch.sh s'assure juste que D-Bus est bien lancé et réutilisé entre les sessions de l'utilisateur:

#!/bin/sh
DBUS_PATH="/tmp/.dbus.`whoami`"
if [ ! -e $DBUS_PATH ]; then
        dbus-launch --sh-syntax > $DBUS_PATH
        chmod 400 $DBUS_PATH
fi
cat $DBUS_PATH

Voilà pour ces cas pratiques. La prochaine fois, je parlerai soit de PubSub, soit de Jingle.

Vus : 526
Publié par Goffi : 100