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 python # Echo server program import socket import shlex, subprocess import sys import os import os.path NOTIFIER = '/usr/bin/notify-send' VIEWER = '/usr/bin/see' 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) return (action, args, data[0]) def notify(summary, message): return [NOTIFIER, summary, message] def send(filename, content): f = open(filename, 'wb') f.write(content) f.close() return [VIEWER , filename] 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.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 export GTK2_RC_FILES="$HOME/.gtkrc-2.0" if [ -x ~/.applications/bin/statnot ]; then killall notification-daemon &> /dev/null ~/.applications/bin/statnot ~/.statnot/config.py & fi; if [ -x /usr/bin/VBoxClient ]; then /usr/bin/VBoxClient --clipboard & fi; if [ -x /usr/bin/parcellite ]; then /usr/bin/parcellite & fi; exec i3
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'.split(' '), stdin=p1.stdout, stdout=subprocess.PIPE) p1.stdout.close() output = p2.communicate()[0]
Et voilà le résultat :
Vous pouvez retrouver l'ensemble des mes fichiers de configuration ici.




