# This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # Copyright (C) 2010, Nicolas Pouillon, # *** IRSSI SOURCE PATCH MANDATORY *** # # In order to be able to get the PUSH commands colloquy sends (in irc # stream), proxy module has to be patched with the (trivial) patch # below. It emits a signal for each PUSH command. # # I did not patch the signals.txt file, but rather added the signal # definition in this file. If patch is integrated, definition should # be moved. # This IRSSI module implements the PUSH notifications for Mobile # Colloquy (http://colloquy.mobi). It assumes you use the irssi-proxy # module. # Features / TODO: # * Pushes to devices as soon as they are offline # # * Stores 10 missed messages to replay the log once device gets back # (other messages are lost for colloquy, but are still present in # irssi). These 10 messages also include messages *not* highlighted, # so an active channel you're on may flush out the interesting # queries... # TODO: keep interesting messages # # * Nicely supports multiple servers or multiple devices, filters-out # non-related messages # # * Saves the script status for later reload # -> nice to hack the script and reload # -> bad if long between the save and the restore # TODO: add a timeout for reloaded devices # # * /push command: # [debug on|debug off]: be more or less verbose # status: prints status of known devices # save: save internal state of the script (implicitely done when # really needed) # # * TODO: Add a night/day filter like the ZNC implementation # # * TODO: Also push for highlights, not only queries and nick # (search for TODO below) use strict; use JSON; use Socket; use IO::Socket::SSL; use vars qw($VERSION %IRSSI); use Irssi; my $patch = <server == NULL || !client->server->connected) { proxy_outdata(client, ":%s NOTICE %s :Not connected to server\n", client->proxy_address, client->nick); END $VERSION = '0.1.1'; %IRSSI = ( authors => 'Nicolas Pouillon', contact => 'nipo-irssi@ssji.net', name => 'colloquy_push', url => 'http://devnull/', description => 'Implement colloquy PUSH with irssi-proxy help', license => 'GPL', changed => '2010-07-23' ); # Plugin global state my $debug = 0; my $byclient = {}; my $devices = {}; sub state_save { open PUSH_STATE, ">$ENV{HOME}/.irssi/push_state.json"; my $kv = {}; $kv->{byclient} = $byclient; $kv->{devices} = $devices; print PUSH_STATE JSON->new->utf8->encode( $kv ); close PUSH_STATE; } sub state_load { open PUSH_STATE, "<$ENV{HOME}/.irssi/push_state.json"; my $str = ; close PUSH_STATE; return unless ( $str ); my %objs = %{JSON->new->utf8->decode($str)}; my @loaded = (); if ($objs{byclient} != "") { $byclient = $objs{byclient}; push(@loaded, "client"); } if ($objs{devices} != "") { $devices = $objs{devices}; push(@loaded, "devices"); } print CLIENTCRAP "Loaded ".join(" and ", @loaded)." states"; } sub device_uid_get { my ($dev) = @_; return $dev->{dev_token} . $dev->{conn_uid}; } sub client_state_get { my ($client) = @_; $byclient->{$client->{_irssi}} = { state => "NEW", dev => {} } unless ( defined $byclient->{$client->{_irssi}} ); my $state = $byclient->{$client->{_irssi}}; print CLIENTCRAP ("Client state for $client->{_irssi}: ".JSON->new->utf8->encode($state)) if ( $debug ); return $state; } sub device_start_new { my ($client, $dev) = @_; my $state = client_state_get($client); $state->{state} = "CTOR"; $state->{dev} = $dev; delete($state->{uid}); print CLIENTCRAP ("Client $client->{_irssi} now has device $dev->{dev_name}") if ( $debug ); } sub client_device_validate { my ($client, $dev) = @_; my $state = client_state_get($client); my $uid = device_uid_get($dev); if ( defined $devices->{$uid} ) { print CLIENTCRAP ("Dev $dev->{dev_name} was already known"); my $missed = $devices->{$uid}->{missed}; my @missed = @{$missed}; print CLIENTCRAP "Replaying log of ".@missed." elements" if ( $debug ); foreach my $msg (@missed) { print CLIENTCRAP " $msg" if ( $debug ); Irssi::signal_emit('proxy client dump', $client, "$msg\r\n"); } } else { print CLIENTCRAP ("Dev $dev->{dev_name} is new"); } $devices->{$uid} = $dev; $dev->{missed} = []; $dev->{failing} = 0; $dev->{server_tag} = $client->{server}->{tag}; $state->{state} = "OK"; $state->{uid} = $uid; delete($state->{dev}); state_save(); } sub device_drop_token { my ($hash) = @_; print CLIENTCRAP ("Dropping all devices matching $hash"); foreach my $key (keys %$devices) { my $dev = $devices->{$key}; next if ( $dev->{dev_token} ne $hash ); print CLIENTCRAP ("Dev $dev->{dev_name} is dropped"); delete $dev->{$key}; } } sub device_list_by_name { my ($name) = @_; my @ret; foreach my $key (keys %$devices) { my $dev = $devices->{$key}; next if ( $dev->{dev_name} ne $name ); push(@ret, $dev); } return @ret; } sub client_device_get { my ($client) = @_; my $state = client_state_get($client); return $devices->{$state->{uid}} if ( $state->{state} =~ /OK/ ); return $state->{dev}; } sub tcp_ssl_put { my ($host, $port, $str) = @_; my $socket = IO::Socket::SSL->new( Domain => &AF_INET, PeerAddr => "$host:$port", Timeout => 2, SSL_verify_mode => 0); if ($socket and $socket->connected()) { print CLIENTCRAP ("Putting $host:$port '$str'") if ( $debug ); print $socket "$str"; close($socket); return 0; } else { print CLIENTCRAP ("Push connection to $host:$port failed"); return 1; } return 0; } sub device_push { my ($dev, $args) = @_; my $kv = {}; $kv->{"device-token"} = $dev->{dev_token}; $kv->{connection} = $dev->{conn_uid}; $kv->{server} = $dev->{conn_name}; while ( my ($k, $v) = each(%$args) ) { $kv->{$k} = $v; } if ( $kv->{badge} ) { $kv->{sound} = $dev->{message_sound}; $kv->{badge} = int($kv->{badge}); } else { $kv->{badge} = "reset"; } my $str = JSON->new->utf8->ascii->encode($kv); print CLIENTCRAP "Pushing $str" if ( $debug ); if ( $dev->{failing} || tcp_ssl_put($dev->{push_host}, $dev->{push_port}, $str) ) { $dev->{failing} = 1; } } sub all_device_push { my ($server, $args, $msg, $important) = @_; print CLIENTCRAP "Pushing message from net $server->{tag}: $msg" if ( $debug ); for my $dev (values %$devices) { print CLIENTCRAP ("Considering $dev->{dev_name}, net $dev->{server_tag}") if ( $debug ); if ($dev->{state} =~ /ONLINE/) { print CLIENTCRAP (" $dev->{dev_name} is online") if ( $debug ); next; } if ( $server->{tag} ne $dev->{server_tag} ) { print CLIENTCRAP (" $dev->{dev_name} is not on right network") if ( $debug ); next; } my $missed = $dev->{missed}; push(@$missed, $msg); shift(@$missed) if ( $#{$missed} > 10 ); device_push($dev, $args) if ( $important ); } state_save(); } sub sig_proxy_push { my ($client, $args) = @_; my $dev = client_device_get($client); if ( $args =~ /^add-device ([a-z0-9]+) :(.*)/ ) { # add-device device_token :name $dev = {}; $dev->{dev_token} = $1; $dev->{dev_name} = $2; device_start_new($client, $dev); } elsif ( $args =~ /^remove-device ([a-zA-Z0-9]+)/ ) { # remove-device device_token device_drop_token($1); } elsif ( $args =~ /^service ([^ ]+) ([0-9]+)/ ) { # service hostname port $dev->{push_host} = $1; $dev->{push_port} = $2; } elsif ( $args =~ /^connection ([A-Za-z0-9]+) :(.*)/ ) { # connection conn_uid :conn_name $dev->{conn_uid} = $1; $dev->{conn_name} = $2; } elsif ( $args =~ /^highlight-sound :(.*)/ ) { # highlight-sound :filename $dev->{highlight_sound} = $1; } elsif ( $args =~ /^message-sound :(.*)/ ) { # message-sound :filename $dev->{message_sound} = $1; } elsif ( $args =~ /^end-device/ ) { # end-device print CLIENTCRAP ("Finished configuration of device ".$dev->{dev_name}); # while ( my ($k, $v) = each(%$dev) ) { # print CLIENTCRAP " $k: $v"; # } $dev->{state} = "ONLINE"; client_device_validate($client, $dev); device_push($dev, { badge => 0 }); } else { print CLIENTCRAP ("unknown push from $client->{host} : $args"); } } sub client_disconn { my ($client) = @_; my $dev = client_device_get($client); return unless defined $dev; print CLIENTCRAP "Device $dev->{dev_name} went offline, we will push messages"; $dev->{state} = "OFFLINE"; delete $byclient->{$client->{_irssi}}; } sub sig_msg_priv { my ($server, $message, $user, $address) = @_; # print CLIENTCRAP "Private message from $user: $message"; all_device_push($server, { sender => $user, message => $message, badge => 1 }, ":$user PRIVMSG $server->{nick} :$message", 1); } sub sig_msg_pub { my ($server, $message, $user, $address, $target) = @_; # TODO: support other highlights my $must_push = ( $message =~ /\b$server->{nick}\b/i ); # print CLIENTCRAP "Public message from $user: $message to $target"; all_device_push($server, { message => $message, badge => 1, room => $target }, ":$user PRIVMSG $target :$message", $must_push); } Irssi::signal_register({ 'proxy push command' => [ "Irssi::Irc::Client", "string" ] }); Irssi::signal_add_first('proxy push command', 'sig_proxy_push'); Irssi::signal_add('proxy client disconnected', 'client_disconn'); Irssi::signal_add('message private', 'sig_msg_priv'); Irssi::signal_add('message public', 'sig_msg_pub'); sub cmd_push { my ($data, $server, $witem) = @_; my @args = split(" ", $data); if ( $args[0] =~ /status/i ) { for my $dev (values %$devices) { my $missed = $dev->{missed}; my @missed = @$missed; my @msg = ("Dev $dev->{dev_name} / $dev->{conn_name}"); push(@msg, $dev->{state}); push(@msg, "$#missed messages waiting") if (@missed); push(@msg, "failing") if ($dev->{failing}); print CLIENTCRAP join(', ', @msg); } } elsif ( $args[0] =~ /save/i ) { state_save(); print CLIENTCRAP "Push state saved"; } elsif ( $args[0] =~ /debug/i ) { if ( $args[1] =~ /on/i ) { $debug = 1; } elsif ( $args[1] =~ /off/i ) { $debug = 0; } print CLIENTCRAP "Push debug is $debug"; } elsif ( $args[0] =~ /resetfail/ ) { for my $dev (device_list_by_name($args[1])) { $dev->{failing} = 0; print CLIENTCRAP "Dev $dev->{dev_name} / $dev->{conn_name} not failing any more"; } } else { print CLIENTCRAP "/PUSH [status|save|debug]"; } } Irssi::command_bind('push', 'cmd_push'); Irssi::command_set_options('push', 'debug status save resetfail'); print CLIENTCRAP "*** Please ensure you are running a patched irssi-proxy instance (See script source for details) ***"; state_load();