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 :
Vous pouvez retrouver l’ensemble des mes fichiers de configuration ici.