RaspBot SleekXMPP

L’idée est de pouvoir contrôler mon RaspberryPi et en particulier activer / visionner ma Vidéo surveillance.

Pour cela je reprend l’idée de connecter un Bot XMMP afin de m’abstenir de connaître mon adresse IP (je ne suis pas en IP fixe) et d’ouvrir une connexion SSH.

Mais en attendant de comprendre comment fonctionne les TAP (Twisted App Plugins) de Twisted, framework réseau, ci-dessous une implémentation du Bot XMPP avec la lib SleekXMPP :

Bot XMPP

Après avoir installé SleekXMPP (# pip install sleekxmpp) :

# vim raspbot-sleekxmpp.py
#! /usr/bin/python -O
# -*- coding: utf-8 -*-
#
# A simple XMPP bot.
# Copyright (C) 2014 MiKael NAVARRO
#

"""A simple XMPP bot.
Copyright (C) 2014 MiKael NAVARRO

A XMPP bot to pilot the Raspberry Pi.
"""

# Specific variables for pydoc
__author__ = "MiKael Navarro <klnavarro@gmail.com>"
__date__ = "Sat Apr 12 2014"
__version__ = "20140412"
__credits__ = """Thanks to Nathan Fritz library <https://github.com/fritzy/SleekXMPP>."""

# Include directives
import sys
import logging
from pprint import pprint as pp

from sleekxmpp import ClientXMPP
from sleekxmpp.exceptions import IqError, IqTimeout

#from subprocess import check_output
import subprocess

if "check_output" not in dir(subprocess):  # duck punch it in!
   def f(*popenargs, **kwargs):
       if 'stdout' in kwargs:
           raise ValueError('stdout argument not allowed, it will be overridden.')
       process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
       output, unused_err = process.communicate()
       retcode = process.poll()
       if retcode:
           cmd = kwargs.get("args")
           if cmd is None:
               cmd = popenargs[0]
           raise subprocess.CalledProcessError(retcode, cmd)
       return output
   subprocess.check_output = f
   
class RaspBot(ClientXMPP):
   """Our Rasp bot is a simple XMPP client.  It will listen for any
   message stanzas and then reply to some of them."""

   def __init__(self, jid, password):
       ClientXMPP.__init__(self, jid, password)

       self.add_event_handler("session_start", self.session_start)
       self.add_event_handler("message", self.message)

       # If you are working with an OpenFire server, you will
       # need to use a different SSL version:
       #import ssl
       #self.ssl_version = ssl.PROTOCOL_SSLv3

   def session_start(self, event):
       self.send_presence()

       # Most get_*/set_* methods from plugins use Iq stanzas, which
       # can generate IqError and IqTimeout exceptions
       try:
           self.get_roster()
       except IqError as err:
           logging.error('There was an error getting the roster')
           logging.error(err.iq['error']['condition'])
           self.disconnect()
       except IqTimeout:
           logging.error('Server is taking too long to respond')
           self.disconnect()

   def message(self, msg):
       """When receiving message stanzas reply to some of them."""
       
       if msg['type'] in ('chat', 'normal'):

           # Return uptime
           if msg['body'].startswith("uptime"):
               uptime = subprocess.check_output("uptime", shell=True)
               msg['body'] = uptime

           # Return public IP address
           elif msg['body'].startswith("ip"):
               try:
                   from duckduckgo import search as ddg
                   yip = ddg("my ip").answer.text  # => Your IP address is <ip> in <location>
                   myip, myloc = (yip.split()[4], yip.split()[6:])

                   msg['body'] = "My IP address is %s in %s" % (myip, " ".join(myloc))
               except:
                   myip = subprocess.check_output("curl ifconfig.me", shell=True)
                   #myip = subprocess.check_output("wget -qO- ifconfig.me/ip", shell=True)
                   #myip = subprocess.check_output("curl ipecho.net/plain", shell=True)

                   msg['body'] = "My IP address is %s" % myip

           # Return top processes
           elif msg['body'].startswith("top"):
               nbproc = 5
               if len(msg['body'].split()) == 2:
                   nbproc += int(msg['body'].split()[1]) + 2
               
               top = subprocess.check_output("top -b -n 3 | head -%d" % nbproc, shell=True)
               msg['body'] = top

           # Return memory used
           elif msg['body'].startswith("mem"):
               mem = subprocess.check_output("free -h", shell=True)
               msg['body'] = "\n"+mem

           # By default, return uptime
           else:
               uptime = subprocess.check_output("uptime", shell=True)
               msg['body'] = uptime
           
           # Reply
           msg.reply("%(body)s" % msg).send()

if __name__ == '__main__':
   # Ideally use optparse or argparse to get JID,
   # password, and log level.

   logging.basicConfig(level=logging.DEBUG,
                       format='%(levelname)-8s %(message)s')

   hostname = subprocess.check_output("uname -n", shell=True)

   xmpp = RaspBot("user@jabber.fr/%s" % hostname, "pass")
   xmpp.connect()
   xmpp.process(block=True)

Ce code est a déployer dans /usr/local/bin/raspbot.py

Init.d script

Puis il est nécessaire de créer un script d’init :

# vim /etc/init.d/raspbot
#! /bin/sh
#
# Subsystem file for "RaspBot" server.
# Copyright (C) 2014 MiKael NAVARRO
#
### BEGIN INIT INFO
# Provides:          RaspBot
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: A simple XMPP bot.
# Description:       A XMPP bot to pilot the Raspberry Pi.
### END INIT INFO

# Do NOT "set -e"

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin
NAME=raspbot
DESC="A XMPP bot to pilot Raspberry Pi."
DAEMON=/usr/local/bin/raspbot.py
DAEMON_OPT=""
SCRIPTNAME=/etc/init.d/$NAME
PIDFILE=/var/run/$NAME.pid
LOGFILE=/var/log/$NAME.log
QUIET="yes"

# Exit if the package is not installed
[ -x "$DAEMON" ] || {
   echo "$DAEMON not installed correctly!"
   exit 0
}

# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME

#
# Function that starts the daemon/service.
# Return 1 if file is locked, else 0
#
start() {
   TEMPFILE=/var/run/$NAME.$$

   # If lock file already exist, test the pid...
   [ -f $PIDFILE ] && kill -0 `cat $PIDFILE` 2>&1 > /dev/null && {
       [ "$QUIET" != "yes" ] && echo "Process already running (pid '`cat $PIDFILE`')!"
       return 1
   }
   # The processus is dead.
   [ -f $PIDFILE ] && {
        [ "$QUIET" != "yes" ] && echo "Removing stale lock file (`cat $PIDFILE`)..."
        rm -f $PIDFILE
   }

   #echo "Starting $NAME..."
   $DAEMON $DAEMON_OPT &

   # Copy the current pid into TEMPFILE.
   { echo $! > $TEMPFILE ; } 2>&1 > /dev/null || {
       [ "$QUIET" != "yes" ] && echo "You don't have permission to access '`dirname $TEMPFILE`'!"
       return 0
   }
   # Create a hard link.
   ln $TEMPFILE $PIDFILE 2>&1 > /dev/null && {
       [ "$QUIET" != "yes" ] && echo "Process locked ($PIDFILE)."
       rm -f $TEMPFILE
       return 1
   }
   # Else, fails for some other reason?
   [ "$QUIET" != "yes" ] && echo "Error!"
   rm -f $TEMPFILE
   return 0
}

#
# Function that stops the daemon/service.
# Return 1 if error, else 0
#
stop() {
   # Check il lock file exist
   [ -f $PIDFILE ] || {
        [ "$QUIET" != "yes" ] && echo "File lock not found ($PIDFILE)!"
        return 1
   }
   # If lock file exist, kill the pid
   kill -TERM `cat $PIDFILE` 2>&1 > /dev/null && {
       [ "$QUIET" != "yes" ] && echo "Process terminated (pid '`cat $PIDFILE`')."
       rm -f $PIDFILE
       return 0
   }
   kill -KILL `cat $PIDFILE` 2>&1 > /dev/null && {
       [ "$QUIET" != "yes" ] && echo "Process killed (pid '`cat $PIDFILE`')."
       rm -f $PIDFILE
       return 0
   }
   # Else, fails for some other reason?
   [ "$QUIET" != "yes" ] && echo "Error!"
   return 1
}

#
# Function that checks the daemon/service.
# Return 1 if running, else 0
#
status() {
   # If lock file exist, test the pid
   [ -f $PIDFILE ] && kill -0 `cat $PIDFILE` 2>&1 > /dev/null && {
       [ "$QUIET" != "yes" ] && echo "Process is running (pid '`cat $PIDFILE`')."
       return 1
   }
   # The processus is dead.
   rm -f $PIDFILE
   [ "$QUIET" != "yes" ] && echo "Process stopped."
   return 0
}

case $1 in
   start)
        start
        [ $? -eq 1 ] && echo "$NAME started, process `cat $PIDFILE`" || echo "$NAME start/fail!"
        ;;
   stop)
        stop
        [ $? -eq 0 ] && echo "$NAME stopped" || echo "$NAME stop/fail!"
        ;;
   restart)
        stop
        [ $? -eq 0 ] && echo "$NAME stopped"
        start
        [ $? -eq 1 ] && echo "$NAME started, process `cat $PIDFILE`" || echo "$NAME start/fail!"
        ;;
   status)
        status
        [ $? -eq 1 ] && echo "$NAME running, process `cat $PIDFILE`" || echo "$NAME not running"
        ;;
   *)
        echo "Usage: $0 <start|stop|restart|status>"
        ;;
esac

Enfin on active le service au boot via : # update-rc.d raspbot defaults