diff --git a/notify.pl b/notify.pl new file mode 100644 index 0000000..a860cae --- /dev/null +++ b/notify.pl @@ -0,0 +1,388 @@ + +# TODO: non expensively detect that X is running + +# TODO: if we have tornado of notifications it looks like when script hits upper +# threads amount limit, some notifications "slip away", and irssi became cpu hog :( + +use strict; +# i do not use warnings pragma, because of irssi (perl) i-threads always generate warnings, +# maybe there is other way to make async jobs in irssi? +#use warnings "all"; +use threads; +use threads::shared; +use MIME::Base64; +use Irssi qw ( + command_bind + settings_get_bool settings_add_bool + settings_get_str settings_add_str +); +use vars qw($VERSION %IRSSI); + +sub notify; +sub hookfn; +sub timeraction; +sub encodestr; +sub notify_set; +sub loadlist($); +sub savelist(@); +sub uniq(@); + +$VERSION = '1.01'; +%IRSSI = ( + authors => 'Eleksir', + contact => 'eleksir@exs-elm.ru', + name => 'notify.pl', + description => 'Desktop notifications.', + license => 'MIT', + changed => 'Sun Apr 16 21:15:25 MSK 2017', +); + +my $help = 'Usage: +/notify enable nick|chan|net - enables apropriate whitelist +/notify disable nick|chan|net - disables apropriate whitelist +/notify status - shows statuses of whitelists +/notify show - shows whitelists and their statuses +/notify add this chan|net - adds currently open channel|network to apropriate whitelist +/notify add nick|chan|net - adds to apropriate whitelist, can be \'.\' for currently open chan or net +/notify del nick|chan|net - removes from apropriate whitelist, can be \'.\' for currently open chan or net'; + +Irssi::command_bind( 'notify' => 'notify_set' ); + +Irssi::signal_add_last("message public", "hookfn"); +Irssi::signal_add_last("message private", "hookfn"); + +# looks like 'notify' here is placeholder, all settings landing to section settings{"perl/core/scripts" = { ->IN_HERE<-} } +Irssi::settings_add_str('notify', 'nicklist', ''); +Irssi::settings_add_str('notify', 'chanlist', ''); +Irssi::settings_add_str('notify', 'netlist', ''); +Irssi::settings_add_bool('notify', 'nicklist_enable', 0); +Irssi::settings_add_bool('notify', 'chanlist_enable', 0); +Irssi::settings_add_bool('notify', 'netlist_enable', 0); +# irssi save settings (via /save) to it's config only if at least one parameter is not default! + +Irssi::signal_emit('setup changed'); # a good practice. irssi actually reads config only if this signal emited, + # so, if someone uses this script' settings, it will use renewed settings + +my $active = 0; share($active); +my $threads = 0; share($threads); +my $THREADS_MAX = 30; +my $hook; + +sub notify { + my ($topic, $message) = @_; + threads->detach(); + $topic =~ s/\\/\\\\/g; + $topic =~ s/\'/\\'/g; + $message =~ s/\\/\\\\/g; + $message =~ s/\'/\\'/g; + system("notify-send", "-u", "normal", "-t", "12000", "-a", "hexchat", $topic, "-i", "hexchat", $message); + $active = 0; +} + +sub notify_set { + my ($argstring, $serverhref, $channelhref) = @_; + my $msg = undef; + + if ($argstring eq 'status') { + my $nicklist = Irssi::settings_get_bool('nicklist'); + my $chanlist = Irssi::settings_get_bool('chanlist'); + my $netlist = Irssi::settings_get_bool('netlist'); + + if (($nicklist == 0) and ($chanlist == 0) and ($netlist == 0)) { + $msg = sprint("All whitelists are disabled, notifications will not appear."); + } else { + $msg = ''; + + if ($nicklist == 0) { $msg .= "Nicks whitelist: disabled\n"; } + else { $msg .= "Nicks whitelist: enabled\n"; } + + if ($chanlist == 0) { $msg .= "Channel whitelist: disabled\n"; } + else { $msg .= "Channel whitelist: enabled\n"; } + + if ($netlist == 0) { $msg .= "Networks whitelist: disabled"; } + else { $msg .= "Networks whitelist: enabled"; } + } + } elsif ($argstring eq 'show') { + $msg = ''; + + if (Irssi::settings_get_bool('nicklist_enable') == 0) { + $msg .= "Nick whitelist: disabled\n"; + } else { + $msg .= "Nick whitelist: enabled\n"; + } + + $msg .= "Whitelisted nicks = " . join( ', ', loadlist('nicklist')) ."\n"; + + if (Irssi::settings_get_bool('chanlist_enable') == 0) { + $msg .= "Channel whitelist: disabled\n"; + } else { + $msg .= "Channel whitelist: enabled\n"; + } + + $msg .= "Whitelisted channels = " . join( ', ', loadlist('chanlist')) . "\n"; + + if (Irssi::settings_get_bool('netlist_enable') == 0) { + $msg .= "Networks whitelist: disabled\n"; + } else { + $msg .= "Networks whitelist: enabled\n"; + } + + $msg .= "Whitelisted networks = " . join( ', ', loadlist('netlist')); + } elsif ($argstring =~ /^enable /) { + if((split(/ +/, $argstring))[1] eq 'nick') { + Irssi::settings_set_bool('nicklist_enable', '1'); + $msg = "Nick whitelist now enabled"; + } elsif ((split(/ +/, $argstring))[1] eq 'chan') { + Irssi::settings_set_bool('chanlist_enable', '1'); + $msg = "Channel whitelist now enabled"; + } elsif ((split(/ +/, $argstring))[1] eq 'net') { + Irssi::settings_set_bool('netlist_enable', '1'); + $msg = "Net whitelist now enabled"; + } + + Irssi::signal_emit('setup changed'); + } elsif ($argstring =~ /^disable /) { + if((split(/ +/, $argstring))[1] eq 'nick') { + Irssi::settings_set_bool('nicklist_enable', '0'); + $msg = "Nick whitelist now disabled"; + } elsif ((split(/ +/, $argstring))[1] eq 'chan') { + Irssi::settings_set_bool('chanlist_enable', '0'); + $msg = "Channel whitelist now disabled"; + } elsif ((split(/ +/, $argstring))[1] eq 'net') { + Irssi::settings_set_bool('netlist_enable', '0'); + $msg = "Net whitelist now disabled"; + } + + Irssi::signal_emit('setup changed'); + } elsif ($argstring =~ /^add /) { + my (undef, $entity, $who) = split(/ +/, $argstring, 3); + + if($entity eq 'nick') { + my @nicks = uniq(loadlist('nicklist'), $who); + savelist('nicklist', @nicks); + Irssi::signal_emit('setup changed'); + $msg = sprintf("Nick whitelist now: %s", join(', ', loadlist('nicklist'))); + } elsif ($entity eq 'chan') { + my @chans = uniq(loadlist('chanlist'), $who); + savelist('chanlist', @chans); + Irssi::signal_emit('setup changed'); + $msg = sprintf("Chan whitelist now: %s", join(', ', loadlist('chanlist'))); + } elsif($entity eq 'net') { + my @nets = uniq(loadlist('netlist'), $who); + savelist('netlist', @nets); + Irssi::signal_emit('setup changed'); + $msg = sprintf("Net whitelist now: %s", join(', ', loadlist('netlist'))); + } elsif ($entity eq 'this') { + if ($who eq 'net') { + if (defined($serverhref)) { + my @nets = uniq(loadlist('netlist'), $serverhref->{'chatnet'}); + savelist('netlist', @nets); + Irssi::signal_emit('setup changed'); + $msg = sprintf("Net whitelist now: %s", join(', ', loadlist('netlist'))); + } else { + $msg = 'Run this command on server or channel window.'; + } + } elsif ($who eq 'chan') { + if (defined($channelhref)) { + my @chans = uniq(loadlist('chanlist'), $channelhref->{'name'}); + savelist('chanlist', @chans); + Irssi::signal_emit('setup changed'); + $msg = sprintf("Chan whitelist now: %s", join(', ', loadlist('chanlist'))); + } else { + $msg = 'You\'re not in a channel window.'; + } + } elsif ($who eq 'nick') { + # looks like if you're chatting with person $channelhref->{'topic'} is not defined + if ((defined($channelhref)) and (! defined($channelhref->{'topic'}))) { + my @nicks = uniq(loadlist('nicklist'), $channelhref->{'name'}); + savelist('nicklist', @nicks); + Irssi::signal_emit('setup changed'); + $msg = sprintf("Nick whitelist now: %s", join(', ', loadlist('nicklist'))); + } else { + $msg = 'You\'re not chatting with person.'; + } + } + } + } elsif ($argstring =~ /^del /) { + my (undef, $entity, $who) = split(/ +/, $argstring, 3); + + if($entity eq 'nick') { + savelist( 'nicklist', map { $_ eq $who ? () : $_ } loadlist('nicklist')); + Irssi::signal_emit('setup changed'); + $msg = sprintf("Nick whitelist now: %s", join(', ', loadlist('nicklist'))); + } elsif ($entity eq 'chan') { + savelist( 'chanlist', map { $_ eq $who ? () : $_; } loadlist('chanlist')); + Irssi::signal_emit('setup changed'); + $msg = sprintf("Chan whitelist now: %s", join(', ', loadlist('chanlist'))); + } elsif ($entity eq 'net') { + savelist( 'netlist', map { $_ eq $who ? () : $_; } loadlist('netlist')); + Irssi::signal_emit('setup changed'); + $msg = sprintf("Net whitelist now: %s", join(', ', loadlist('netlist'))); + } elsif ($entity eq 'this') { + if ($who eq 'net') { + if (defined($serverhref)) { + savelist( 'netlist', map { $_ eq $serverhref->{'chatnet'} ? () : $_; } loadlist('netlist')); + Irssi::signal_emit('setup changed'); + $msg = sprintf("Net whitelist now: %s", join(', ', loadlist('netlist'))); + } else { + $msg = 'Run this command on server or channel window.'; + } + } elsif ($who eq 'chan') { + if (defined($channelhref)) { + savelist( 'chanlist', map { $_ eq $channelhref->{'name'} ? () : $_; } loadlist('chanlist')); + Irssi::signal_emit('setup changed'); + $msg = sprintf("Chan whitelist now: %s", join(', ', loadlist('chanlist'))); + } else { + $msg = 'You\'re not in a channel window.'; + } + } elsif ($who eq 'nick') { + if ((defined($channelhref)) and (! defined($channelhref->{'topic'}))) { + savelist( 'nicklist', map { $_ eq $channelhref->{'name'} ? () : $_; } loadlist('nicklist')); + Irssi::signal_emit('setup changed'); + $msg = sprintf("Nick whitelist now: %s", join(', ', loadlist('nicklist'))); + } else { + $msg = 'You\'re not chatting with person.'; + } + } + } + } else { + $msg = $help; + } + + Irssi::active_win()->print($msg); +} + +sub hookfn { + my ($server, $message, $nick, $nick_address, $chan) = @_; + my $nicklist_enable = Irssi::settings_get_bool('nicklist_enable'); + my $chanlist_enable = Irssi::settings_get_bool('chanlist_enable'); + my $netlist_enable = Irssi::settings_get_bool('netlist_enable'); + my $flag = 1; # show notification + $flag = 0 if (($nicklist_enable != 1) or ($chanlist_enable != 1) or ($netlist_enable != 1)); + + if (($nicklist_enable != 0) and ($flag == 0)) { + my @nicklist = loadlist('nicklist'); + + foreach (@nicklist) { + next unless(defined($_)); + next if($_ eq ''); + + if ($_ eq $nick) { + $flag = 1; + last; + } + } + + @nicklist = -1; undef @nicklist; + } + + if (($chanlist_enable != 0) and ($flag == 0)) { + my @chanlist = loadlist('chanlist'); + + foreach (@chanlist) { + next unless(defined($_)); + next if($_ eq ''); + + if ($_ eq $chan) { + $flag = 1; + last; + } + } + + @chanlist = -1; undef @chanlist; + } + + if (($netlist_enable != 0) and ($flag == 0)) { + my @netlist = loadlist('netlist'); + + foreach (@netlist) { + next unless(defined($_)); + next if($_ eq ''); + + if ($_ eq $server){ + $flag = 1; + last; + } + } + + @netlist = -1; undef @netlist; + } + + if ($flag == 1) { + $active = 1; + $message = Irssi::strip_codes($message); + $message = encodestr($message); + $nick = Irssi::strip_codes($nick); + my $topic = sprintf("%s at %s says:\n", $nick, $chan); + + # perl can create limited amount of threads, so if there are a lot of notifications + # we have to use infinite loop with sleep + my $caught = 0; + + do { + eval { threads->create(\¬ify, $topic, $message) or die; }; + + if ($@) { + $caught = 1; + sleep 1; + } else { + $caught = 0; + } + } while ( $caught == 1 ); + + if ($active == 1) { + # implement async action + $hook = Irssi::timeout_add(250, "timeraction", ''); + } + } + + undef $nicklist_enable; undef $chanlist_enable; undef $netlist_enable; + undef $flag; +} + +sub timeraction { + if (($active == 0) and (defined($hook))) { + Irssi::timeout_remove($hook); + } +} + +sub encodestr { + my $str = shift; + $str = join('', map { + if ($_ eq '<') { $_ = '<'; } + elsif ($_ eq '>') { $_ = '>'; } + elsif ($_ eq '&') { $_ = '&'; } + elsif ($_ eq '"') { $_ = '"'} + else { $_ = $_; } + } split(//, $str)); + + return $str; +} + +sub loadlist($){ + my $setting = shift; + my @values; + my $val = Irssi::settings_get_str($setting) or do { + Irssi::settings_set_str($setting, ''); + undef $setting; + return @values; # empty array :) + }; + + @values = map { decode_base64($_); } split(/ /, $val); + undef $setting; undef $val; + return @values; +} + +sub savelist(@) { + my $setting = shift; + my @list = map { encode_base64($_, ''); } @_; + my $value = join(' ', @list); + Irssi::settings_set_str($setting, $value); +} + +sub uniq(@) { + my %hash = map { $_ => 1 } @_; + return keys %hash; +}; + +# vim: set noai ts=4 sw=4: