Friday, January 3, 2014

IRC notifications from remote server

Like many people, I use irssi client on remote server to connect to IRC chat. With tmux or GNU Screen you are able to be connected 24/7, and it would be great to be notified when someone is writing to you or has mentioned you on a channel.
Read more on how to setup such a scenario.

In my case, I use 4 basic components.
  1. slightly modified notify.pl script, to capture and send notifications from IRC
  2. stunnel to encrypt communication between remote hosts
  3. xinetd service, which listens on some port and triggers action when you receive message from IRC sever
  4. ircnotify script, which is called by the xinetd and creates notification
NOTE: for this to work, you have to be able to access your local PC from remote server. Following picture demonstrates how are components connected:

SETTING UP REMOTE SERVER:

1. notify.pl

You can have a look at notify.pl here. My version is modified so it does not call dbus-send, but instead it sends output to network socket created by stunnel.
##
## 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 HTML::Entities;

$VERSION = "0.5";
%IRSSI = (
    authors     => 'Luke Macken, Paul W. Frields',
    contact     => 'lewk@csh.rit.edu, stickster@gmail.com',
    name        => 'notify.pl',
    description => 'Use D-Bus to alert user to hilighted messages',
    license     => 'GNU General Public License',
    url         => 'http://code.google.com/p/irssi-libnotify',
);

Irssi::settings_add_str('notify', 'notify_remote', '');
Irssi::settings_add_str('notify', 'notify_debug', '');

sub sanitize {
  my ($text) = @_;
  encode_entities($text,'\'<>&');
  my $apos = "'";
  my $aposenc = "\'";
  $text =~ s/$apos/$aposenc/g;
  $text =~ s/"/\\"/g;
  $text =~ s/\$/\\\$/g;
  $text =~ s/\#/\\\#/g;
  $text =~ s/\;/\\\;/g;
  $text =~ s/\(/\\\(/g;
  $text =~ s/\)/\\\)/g;
  $text =~ s/`/\\"/g;
  return $text;
}

sub notify {
    my ($server, $summary, $message) = @_;

    # Make the message entity-safe
    $summary = sanitize($summary);
    $message = sanitize($message);

    my $cmd = "EXEC - echo " . $summary . ":" . $message . " | ncat 127.0.0.1 2234";
    #"dbus-send --session /org/irssi/Irssi org.irssi.Irssi.IrssiNotify" .
    #" string:'" . $summary . "'" .
    #" string:'" . $message . "'";
    $server->command($cmd);

    my $remote = Irssi::settings_get_str('notify_remote');
    my $debug = Irssi::settings_get_str('notify_debug');
    my $nodebugstr = '- ';
    if ($debug ne '') {
    $nodebugstr = '';
    }
    if ($remote ne '') {
    my $cmd = "EXEC - echo " . $summary . ":" . $message . " | ncat 127.0.0.1 2234";
    #my $cmd = "EXEC " . $nodebugstr . "ssh -q " . $remote . " \"".
    #    " ~/bin/irssi-notifier.sh".
    #    " dbus-send --session /org/irssi/Irssi org.irssi.Irssi.IrssiNotify" .
    #    " string:'" . $summary . "'" .
    #    " string:'" . $message . "'\"";
    #print $cmd;
    $server->command($cmd);
    }

}

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/ ;
    $stripped =~ s/^\<.[^\>]+\>.// ;
    my $summary = $dest->{target} . ": " . $sender;
    notify($server, $summary, $stripped);
}

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

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

sub dcc_request_notify {
    my ($dcc, $sendaddr) = @_;
    my $server = $dcc->{server};

    return if (!$dcc);
    notify($server, "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');

Next step is to copy notify.pl to ~/.irssi/scripts and create link to it in ~/.irssi/scripts/autorun to load it automatically. Next go to the irssi and execute:
  • /load perl 
  • /script load notify 
Note: you may need to install additional package libhtml-parser-perl (on debian)

2.1 stunnel on remote server

First thing to setup stunnel is to generate certificate and private key with following commands:
openssl req -new -x509 -days 365 -nodes -config stunnel.cnf -out stunnel.pem -keyout stunnel.pem
openssl gendh 2048 >> stunnel.pem

This will create file stunnel.pem, which you have to copy on the remote server and change permissions to 600.
To start stunnel:
stunnel -c -p ~/stunnel.pem -d 2234 -r LOCAL_MACHINE_IP:2233 -P ~/stunnel.pid>
Now on the remote server you will have 0.0.0.0:2234 socket, which will encrypt and send traffic to LOCAL_MACHINE_IP:2233

SETTING UP LOCAL MACHINE:

2.2 stunnel on local machine

On local machine, copy stunnel.pem file to /etc/stunnel/, change owner to root and permissions to 600. Next edit /etc/stunnel/stunnel.conf set the cert path:
  • cert = /etc/stunnel/stunnel.pem
and add section for ircnotify:
[ircnotify]
client=no
accept = 2233
connect = 2234
Start stunnel service and you should get 0.0.0.0:2233 socket on your system.

3. xinetd service
To create xinetd service, create new file /etc/xinetd.d/ircnotify:
service ircnotify
{
        socket_type = stream
        port = 2234
        protocol = tcp
        wait = no
        user = krisko
        server = /home/krisko/bin/ircnotify
        log_on_failure += USERID
        disable = no
}
This defines new service ircnotify which will receive and handle network stream on port 2234 and will execute script /home/krisko/bin/ircnotify as user krisko. One more thing before you can start xinetd service is to add to file /etc/services:
ircnotify       2234/tcp
Now you can start xinetd service and you should have 0.0.0.0:2233 socket.
NOTE: make sure that script you want to run with xinetd is executable.

4. ircnotify script

This script should be enabled with KDE's autostart feature, so it can create fifo pipe to receive and display notifications in KDE. For other DE's you have to enable this script to start after login.

Short description:
When called without parameters, script creates fifo pipe where all further notifications will be sent and displayed.
Basic functions:
After receiving message, notification is shown (via notify-send) and a sound is played (via cvlc) after additional messages being received (from the same sender, only sound is played (5x, specified in maxcount variable). After this, further notifications will be shown after 10 minutes timeout, this is specified on line 70 in ircnotify:
#!/bin/bash

#set variables
icon=/home/krisko/.icons/openterm.png
sound=/usr/share/sounds/KDE-Im-Message-In.ogg
pipe=/tmp/ircnotify_fifo
#maxcount for sound notifications
maxcount=5

notify(){
    title="$1"
    mgs="$2"

    echo "$title::$msg::$icon" > $pipe
}

playsnd(){
    cvlc -q --no-loop --play-and-exit $sound 2>/dev/null
}

#here we create fifo and display notifications
#syntax for pipe msg: title::your message::icon_path
ircnotify_fifo(){
    trap "rm -f $pipe" EXIT

    mkfifo $pipe

    while true
    do
        if read line <$pipe; then
            title=$(echo $line | awk -F:: '{print $1}')
            mgs=$(echo $line | awk -F:: '{print $2}')
            icon=$(echo $line | awk -F:: '{print $3}')
            notify-send "$title" \ "$mgs" -i "$icon"
        fi  
    done
}

usage(){
cat << EOF
KrisKo 2013

Script for receiving notifications from xinetd service
from remote irssi client via customized notify.pl

USAGE:
  -call W/O parameters to create and start notification fifo
      -this should be started with KDE's (or other DE's) autostart service
  -read from STDIN (via | redirection)
      -this will parse message and create notification/play sound
      -note that "::" is used to parse variables, it can cause problems when received from IRC
EOF
}

main(){
    msg="$1"

    #true if this is only highlight
    if [[ "$msg" =~ ^#.*$ ]]; then
        title="IRC Mention"
        msg=$(echo "$msg" | awk -F: '{ st = index($0,":"); print substr($0,st+1)}')
        notify "$title" "$msg"
        playsnd
    #else we have new message
    else
        title=$(echo "$msg" | awk -F: '{ st = index($0,":");print $1}')
        msg=$(echo "$msg" | awk -F: '{ st = index($0,":"); print substr($0,st+1)}')

        #if the file is older than X minutes, remove it 
        find /tmp -maxdepth 1 -mmin +10 -name ircnotify-"$title" -exec rm {} \;

        #notify if this is new notification
        if [ ! -f /tmp/ircnotify-"$title" ]; then
            echo 1 > /tmp/ircnotify-"$title"
            notify "$title" "$msg"
            playsnd
        else
            #only notify for the first X messages
            count=$(cat /tmp/ircnotify-"$title")
            if [ $count -lt $maxcount ]; then
                echo $(($count+1)) > /tmp/ircnotify-"$title"
                playsnd
            fi
        fi
    fi
}

####
# Beginning
####

if [ $# -eq 0 ] && [ ! -p "$pipe" ]; then
    ircnotify_fifo
    exit 0
elif [ $# -eq 1 ] && [ "$1" == "help" ]; then
    usage
    exit 2
elif [ -t 0 ]; then
    echo "You need to provide input for this script"
    echo "Use \"$0 help\" for help"
    exit 1
elif [ ! -t 0 ] && [ -p "$pipe" ]; then
    #read message from input
    read msg
    #call main function
    main "$msg"
else
    echo "Could not connect to ircnotify_fifo, run $0"
    echo "Use \"$0 help\" for help"
    exit 1
fi

exit 0

1 comment: