Notification irssi over SSH

Depuis que je possède un serveur, j’ai en permanence un screen qui tourne avec irssi pour suivre ce qu’il se passe sur IRC. Dernièrement j’utilise aussi ce screen au travail en collaboration avec Puisque non somme en mesure d’envoyer des messages, le travail nécessaire pour envoyer un fichier est relativement faible.

Nous retrouvons notre script côté client qui lance des commandes selon la première ligne reçue : bitlbee pour discuter avec mes collègues via gtalk. Autant les messages qui me sont destinés sur IRC peuvent attendre que je retourne voir mon screen, autant ceux de gtalk doivent mettre notifiés de manière visible.

Pour ce faire, il va falloir mettre en place, côté serveur, un script qui envoie le message pour le récupérer côté client et l’afficher à l’écran. Travaillant sous i3 cela à compliqué la tâche.

Côté serveur

Cette partie est librement inspirée de On-screen notifications from IRSSI over SSH puis récrite en python pour y inclure la gestion des pièces jointes.

Le script suivant, à placer dans le répertoire ~/.irssi/script, se contente d’appeler la commande notify-send lorsqu’un message est mis en avant ou que vous recevez un message privé.

##
## Put me in ~/.irssi/scripts, and then execute the following in irssi:
##
##       /load perl
##       /script load notify
##

use strict;
use Irssi;
use vars qw($VERSION %IRSSI);
use IO::Socket;

$VERSION = "0.2";
%IRSSI = (
    authors     => 'Bernard `Guyzmo` Pratz, Luke Macken, Paul W. Frields',
    contact     => 'guyzmo AT m0g DOT net, lewk@csh.rit.edu, stickster@gmail.com',
    name        => 'notify.pl',
    description => 'Use libnotify over SSH to alert user for hilighted messages',
    license     => 'GNU General Public License',
    url         => 'http://github.com/guyzmo/irssi-over-ssh-notifications',
);

sub notify {
    my ($summary, $message) = @_;
    $summary =~ s/'/'"'"'/g ;
    $message =~ s/'/'"'"'/g ;
    system("notify-send '$summary' '$message'");
}

sub print_text_notify {
    my ($dest, $text, $stripped) = @_;
    my $server = $dest->{server};

    return if (!$server || !($dest->{level} & MSGLEVEL_HILIGHT));
    my $sender = $stripped;
    $sender =~ s/^\<.([^\>]+)\>.+/\1/ ;
    my $summary = $sender . "@" . $dest->{server}->{tag} . $dest->{target};

    $stripped =~ s/^\<.[^\>]+\>.// ;
    notify($summary, $stripped);
}

sub message_private_notify {
    my ($server, $msg, $nick, $address) = @_;

    return if (!$server);
    notify("PM from ".$nick, $msg);
}

sub dcc_request_notify {
    my ($dcc, $sendaddr) = @_;

    return if (!$dcc);
    notify("DCC ".$dcc->{type}." request", $dcc->{nick});
}

Irssi::signal_add('print text', 'print_text_notify');
Irssi::signal_add('message private', 'message_private_notify');
Irssi::signal_add('dcc request', 'dcc_request_notify');

notify-send est normalement fournit par le paquet libnotify-bin est permet d’afficher un message à l’écran. Ici nous allons le remplacer par un script qui envoie le message sur le port 8088 en local :

#!/usr/bin/env python

import sys, socket

HOST = 'localhost'
PORT = 8088
SUBJECT = sys.argv[1]
MESSAGE = sys.argv[2]

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall('NOTIFY ' + SUBJECT + '\n' + MESSAGE)
s.close()

Côté Client

Côté client, il faut commencer par rediriger le port distant 8088 vers le même port en local, grâce à ces quelques lignes placées dans ~/.ssh/config :

Host irc.homecomputing.fr
    PermitLocalCommand yes
    LocalCommand ~/.ssh/ssh-listener.py
    RemoteForward 8088 localhost:8088

Couplé à un client qui va écouter le port pour renvoyer le message au système de notification local :

#!/usr/bin/env python2

# Echo server program
import socket
import shlex, subprocess
import sys
import os
import os.path

NOTIFIER  = '/usr/bin/notify-send'
VIEWER  = '/usr/bin/xdg-open'
OPENER = '/usr/bin/xdg-open'
HOST = 'localhost'
PORT = 8088

# Daemonization
PID_FILE = "/tmp/ssh-listener.pid"
UMASK = 766
WORKDIR='/tmp/'
MAXFD=1024

if (hasattr(os, "devnull")):
   REDIRECT_TO = os.devnull
else:
   REDIRECT_TO = "/dev/null"

def createDaemon():
   """Detach a process from the controlling terminal and run it in the
   background as a daemon.
   """

   try:
      pid = os.fork()
   except OSError, e:
      raise Exception, "%s [%d]" % (e.strerror, e.errno)
   if (pid == 0):	# The first child.
      os.setsid()
      try:
         pid = os.fork()	# Fork a second child.
      except OSError, e:
         raise Exception, "%s [%d]" % (e.strerror, e.errno)
      if (pid == 0):	# The second child.
         os.chdir(WORKDIR)
         os.umask(UMASK)
      else:
         os._exit(0)	# Exit parent (the first child) of the second child.
   else:
      os._exit(0)	# Exit parent of the first child.
   import resource		# Resource usage information.
   maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
   if (maxfd == resource.RLIM_INFINITY):
      maxfd = MAXFD
   # Iterate through and close all file descriptors.
   for fd in range(0, maxfd):
      try:
         os.close(fd)
      except OSError:	# ERROR, fd wasn't open to begin with (ignored)
         pass
   # This call to open is guaranteed to return the lowest file descriptor,
   # which will be 0 (stdin), since it was closed above.
   os.open(REDIRECT_TO, os.O_RDWR)	# standard input (0)
   # Duplicate standard input to standard output and standard error.
   os.dup2(0, 1)			# standard output (1)
   os.dup2(0, 2)			# standard error (2)
   return(0)

def parse(data):
    data = data.split('\n', 1)
    (action, args) = data.pop(0).split(' ', 1)
    if len(data) > 0:
        data = data[0]
    else:
        data = ''
    return (action, args, data)

def action_notify(summary, message):
    return [NOTIFIER, summary, message]

def action_send(filename, content):
    f = open(filename, 'wb')
    f.write(content)
    f.close()
    return [VIEWER , filename]

def action_open(url, content):
    return [OPENER , url]

if __name__ == '__main__':
    fg = False

    ### check arguments

    # check for help
    if len(sys.argv) == 2 and sys.argv[1] in ('-h', '--help') or len(sys.argv) > 2 :
        print '''Usage: %s [-s|-f|-h]
Usage: %s [--stop|--foreground|--help]

Running with no argument or one wrong argument, will still launch the daemon.
Only one argument is expected. More will give you that help message.

    -s|--stop           stop the running daemon
    -f|--foreground     executes in foreground (and outputs all notifications to stdout)
    -h|--help           this help message
''' % (sys.argv[0], sys.argv[0])
        sys.exit(0)

    # check for -stop
    if len(sys.argv) == 2 and sys.argv[1] in ('--stop', '-s'):
        if not os.path.isfile(PID_FILE):
            print 'nothing to stop. exiting...'
            sys.exit(1)
        try:
            os.kill(int(open(PID_FILE, 'r').read()), 9)
        except ValueError, ve:
            print 'Invalid PID file. exiting...'
            sys.exit(1)
        except OSError, oe:
            print 'Invalid PID: %s. Process has already exited. exiting...' % int(open(PID_FILE, 'r').read())
            sys.exit(1)
        os.unlink(PID_FILE)
        print 'notify daemon killed'
        sys.exit(0)

    if os.path.isfile(PID_FILE):
        print 'Daemon is already running... Exiting.'
        sys.exit(1)

    if not (len(sys.argv) == 2 and sys.argv[1] in ('-f', '--foreground')):
        print 'Starting server as daemon...'
        retCode = createDaemon()

        # create PID file
        f = open(PID_FILE, 'w').write(str(os.getpid()))
    else:
        fg = True
        print 'Starting server in foreground mode...'

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind((HOST, PORT))
    s.listen(1)
    if fg is True: print 'Listening on '+str(HOST)+':'+str(PORT)+'...'

    # daemon main loop
    while True:
        data = ''
        conn, addr = s.accept()
        if fg is True: print 'RCPT'
        while 1:
            tmp = conn.recv(1024)
            if not tmp: break
            data += tmp
        conn.close()

        (action, args, data) = parse(data)
        function = locals()['action_'+action.lower()]
        p = subprocess.Popen(function(args, data))

    sys.exit(retCode)

Si vous utilisez un gestionnaire de fenêtre tel que Gnome, vous pouvez vous arrêtez là, notify-send faisant le reste. Cependant si vous utilisez un gestionnaire de fenêtre léger (ici, i3) la suite vous permettra d’afficher le message.

notify-send envoie les messages à un daemon qui se charge de les afficher, comme il ne fait pas le travail que l’on souhaite, nous allons le remplacer par statnot, plus configurable, dans notre fichier ~/.xinitrc :

#!/bin/sh

. "$HOME/.bash_aliases"

export XDG_MENU_PREFIX="lxde-"
export GTK2_RC_FILES="$HOME/.gtkrc-2.0"

if [ -x ~/.local/bin/statnot ]; then
    killall notification-daemon &> /dev/null
    ~/.local/bin/statnot ~/.statnot/config.py &
fi;

if [ -x /usr/bin/davmail ]; then
    /usr/bin/davmail &
fi;

if [ -x /usr/bin/icedove ]; then
    /usr/bin/icedove &
elif [ -x /usr/bin/thunderbird ]; then
    /usr/bin/thunderbird &
fi;

if [ -x /usr/bin/x-www-browser ]; then
    /usr/bin/x-www-browser &
fi;

if [ -x /usr/bin/x-terminal-emulator ]; then
    xirc &
    /usr/bin/x-terminal-emulator -name term-scratchpad &
fi;

if [ -x /usr/bin/xautolock ]; then
    /usr/bin/xautolock -locker '/usr/bin/i3lock -t -i ~/.config/i3/lockscreen.png' &
fi;

exec i3 --force-xinerama

Il nous reste plus qu’à configurer statnot correctement, pour i3 j’utilise dzen2 :

# Default time a notification is show, unless specified in notification
DEFAULT_NOTIFY_TIMEOUT = 0 # milliseconds

# Maximum time a notification is allowed to show
MAX_NOTIFY_TIMEOUT = 0 # milliseconds

# Maximum number of characters in a notification.
NOTIFICATION_MAX_LENGTH = 100 # number of characters

# Time between regular status updates
STATUS_UPDATE_INTERVAL = 2.0 # seconds

# Command to fetch status text from. We read from stdout.
# Each argument must be an element in the array
# os must be imported to use os.getenv
import os
STATUS_COMMAND = ['/bin/sh', '%s/.statusline.sh' % os.getenv('HOME')]

# Always show text from STATUS_COMMAND? If false, only show notifications
USE_STATUSTEXT=True

# Put incoming notifications in a queue, so each one is shown.
# If false, the most recent notification is shown directly.
QUEUE_NOTIFICATIONS=True

# update_text(text) is called when the status text should be updated
# If there is a pending notification to be formatted, it is appended as
# the final argument to the STATUS_COMMAND, e.g. as $1 in default shellscript

# dwm statusbar update
import subprocess
def update_text(text):
    if text:
        p1 = subprocess.Popen(['echo', text], stdout=subprocess.PIPE)
        p2 = subprocess.Popen('dzen2 -p 3 -fg white -bg darkred -xs 1'.split(' '), stdin=p1.stdout, stdout=subprocess.PIPE)
        p1.stdout.close()
        output = p2.communicate()[0]

Et voilà le résultat :

Irssi notification over SSH

Vous pouvez retrouver l’ensemble des mes fichiers de configuration ici.