# $Id: Gnotify.pm,v 1.1 2007/01/26 02:13:30 altblue Exp $ package N0i::Xchat::Gnotify; use strict; use warnings; our $VERSION = sprintf '1.%d.%d', '\$Revision: 1.1 $' =~ /(\d+)\.(\d+)/xm; use English qw( -no_match_vars ); use base qw(N0i::Xchat::Base); use Sub::Installer; use Gtk2::Notify (); use HTML::Entities (); use Time::HiRes (); sub version { return '0.5.9' } sub new { my ( $class, %o ) = @_; $class->_init_commands; my $self = {}; bless $self, $class; $self->load_cf; $self->{_notes} = {}; Gtk2::Notify->init( $self->package ); $self->_init; return $self; } sub load_cf_default { my $self = shift; my %defaults = ( on => 1, # plugin on/off timeout => 3, # notification windows timeout urgency => 'low', # 'low', 'normal', 'critical' # Events to monitor: pubmsg => 1, # public chatter privmsg => 1, # private messages notice => 1, # NOTICEs action => 1, # ACTIONs join => 1, # channel JOIN part => 1, # channel PART topic => 1, # channel TOPIC change kick => 1, # channel KICKs quit => 1, # irc QUIT ctcp => 1, # CTCP events # IRC Server (AND current XChat version) triggers a "+" or "-" before messages? # ^ "identified" vs "unidentified" user uident => 1, # ignore unidentified users? ignore_unidentified => 0, ## IGNORE stuff... # - this is a fucking Regular Expression !! ignore => qr/^(?i:\#.*?(?i:warez|music|mp3).*)$/xm, ); while ( my ( $k, $v ) = each %defaults ) { $self->{$k} = $v; } return; } sub icon { my ( $self, $event, $target, $nick ) = @_; return $self->icon_default if !$event; my @icons_map = (); if ( defined $target && $target =~ /^[#&](.+)$/xm ) { push @icons_map, [ 'channels', $1 ]; } elsif ( defined $nick && $nick =~ /\S/xm ) { push @icons_map, [ 'avatars', $nick ]; } elsif ($event) { push @icons_map, [ 'events', $event ]; } for my $id (@icons_map) { my $icon = File::Spec->catfile( $self->icons_dir, $id->[0], $id->[1] . '.png' ); return $icon if -f $icon; } return $self->icon_default; } sub icons_dir { return File::Spec->catdir( Xchat::get_info('xchatdir'), 'icons' ); } sub icon_default { my $self = shift; return File::Spec->catfile( $self->icons_dir, 'xchat.png' ); } sub get_cached_note { my ( $self, $summary ) = @_; return exists $self->{_notes}{$summary} ? $self->{_notes}{$summary} : undef; } # FIXME: looks like it needs some refactoring! sub notify { my ( $self, %o ) = @_; return if !defined $o{summary}; return if $o{who} && $o{who} eq Xchat::get_info('nick'); if (!defined $o{body}) { $o{body} = q{}; } # current notification cached Hash my $note = $self->get_cached_note( $o{summary} ); # current notify object my $n = $note ? $note->{n} : undef; if ( $note ) { $n->set_timeout(0); # keep it alive if ($o{body} =~ /\S/xm) { if ($note->{body} =~ /\S/xm) { $note->{body} .= "\n" . $o{body}; } else { $note->{body} = $o{body}; } } my @args = ($note->{summary}); if ( $note->{body} =~ /\S/xms ) { push @args, $note->{body}; push @args, $o{icon} || $self->icon( $o{event}, $o{target}, $o{who} ); } else { if ( $o{icon} ) { push @args, q{}, $o{icon}; } } $note->{updated} = Time::HiRes::time(); $n->update(@args); } else { my @args = ( $o{summary} ); if ( $o{body} =~ /\S/xm ) { push @args, $o{body}; push @args, $o{icon} || $self->icon( $o{event}, $o{target}, $o{who} ); } else { if ( $o{icon} ) { push @args, q{}, $o{icon}; } } my $id = $n = Gtk2::Notify->new(@args); $n->signal_connect( closed => \&_note_closed, [ $self, $o{summary} ] ); $note = $self->{_notes}{$o{summary}} = { summary => $o{summary}, body => $o{body}, n => $n, created => Time::HiRes::time(), }; } $n->set_timeout( ( $o{timeout} || $self->{timeout} ) * 1000 ); $n->set_urgency( $o{urgency} || $self->{urgency} ); $n->show; return; } sub _note_closed { my ( $n, $data ) = @_; my ( $self, $id ) = @{$data}; delete $self->{_notes}{$id}; } sub esc { return @_ ? HTML::Entities::encode( "@_", q{&<>"} ) : q{}; } sub nick { return shift =~ /^:([^!]+)/xm ? esc($1) : q{}; } sub userhost { return shift =~ /^:[^!]+!(\S+)/xm ? esc($1) : q{}; } sub linkify { my $url = shift; ( my $text = $url ) =~ s{^.+?//}{}xms; if ( length $text > 50 ) { for ($text) { s/\?.+/?.../xms; s{^(.+?//[^/]+).*(/[^/]+\?)}{$1/...$2}xms; } } $text =~ s{/+$}{}xms; return qq{$text}; } sub clean { return q{} if !@_; my $msg = join q{ }, grep { defined } @_; for ($msg) { s/\s+/ /xmsg; s/[\x00-\x1F]//xmsg; s{^:}{}xms; s{(?{on} || !$self->{$event} || Xchat::get_info('win_status') eq 'active' || $who eq Xchat::get_info('nick') || $who =~ $self->{ignore} || $target =~ $self->{ignore}; if ( $self->{uident} && $self->{ignore_unidentified} && $msg =~ /^:-/xm ) { return; } $msg = clean($msg); if ( $self->{uident} ) { $msg =~ s{^[+-]}{}xm; } return if $msg !~ /\S/xm; return $msg ? $msg : $msg . q{ }; } sub privmsg_handler { my ( $self, $words, $nth, $data ) = @_; my $nick = nick( $words->[0] ); my $target = clean( $words->[2] ); my $msg = $nth->[3]; if ( $msg =~ m/^:([+-]?)\x01ACTION\b(.*)\x01$/xm ) { return $self->_action_handler( $nick, $target, ":$1$2" ); } elsif ( $msg =~ m/^:([+-]?)\x01(.*)\x01$/xm ) { return $self->_ctcp_handler( $nick, $target, ":$1$2" ); } my $event = $target eq Xchat::get_info('nick') ? 'privmsg' : 'pubmsg'; $msg = $self->worthy( $msg, $nick, $target, $event ); return if !$msg; return $self->notify( who => $nick, event => $event, target => $target, summary => $event eq 'privmsg' ? $nick : $target, body => $event eq 'privmsg' ? $msg : "$nick: $msg", ); } sub _action_handler { my ( $self, $nick, $target, $msg ) = @_; $msg = $self->worthy( $msg, $nick, $target, 'action' ); return if !$msg; $msg =~ s{^\s+}{}xm; return $self->notify( who => $nick, event => 'action', target => $target, summary => $target eq Xchat::get_info('nick') ? $nick : $target, body => "*$nick $msg", ); } sub _ctcp_handler { my ( $self, $nick, $target, $msg ) = @_; $msg = $self->worthy( $msg, $nick, $target, 'ctcp' ); return if !$msg; return $self->notify( who => $nick, event => 'ctcp', target => $target, summary => $target eq Xchat::get_info('nick') ? $nick : $target, body => "*** Received a CTCP $msg from $nick", ); } sub notice_handler { my ( $self, $words, $nth, $data ) = @_; my $nick = nick( $words->[0] ); my $target = clean( $words->[2] ); my $msg = $self->worthy( $nth->[3], $nick, $target, 'notice' ); return if !$msg || $words->[0] !~ /^:/xm || $nth->[3] =~ /^:\*{3}/xm; my $summary = $target; if ( $target eq Xchat::get_info('nick') ) { $summary = $nick; $msg = "$msg"; } else { $msg = "$nick: $msg"; } return $self->notify( who => $nick, event => 'notice', target => $target, summary => $summary, body => $msg, ); } sub join_handler { my ( $self, $words, $nth, $data ) = @_; my $nick = nick( $words->[0] ); my $chan = clean( $words->[2] ); my $msg = $self->worthy( 'X', $nick, $chan, 'join' ); return if !$msg; return $self->notify( who => $nick, event => 'join', target => $chan, summary => $chan, body => "*** $nick joined $chan", ); } sub part_handler { my ( $self, $words, $nth, $data ) = @_; my $nick = nick( $words->[0] ); my $chan = clean( $words->[2] ); my $reason = $self->worthy( ( $nth->[3] || q{} ) . 'X', $nick, $chan, 'part' ); return if !$reason; $reason =~ s{X$}{}xm; if ( $reason =~ /\S/xm ) { $reason = ": $reason"; } return $self->notify( who => $nick, event => 'part', target => $chan, summary => $chan, body => "*** $nick left $chan$reason", ); } # FIXME: discover "last seen in WHAT channel" sub quit_handler { my $self = shift; # $self->_debug('QUIT', \@_); my ( $words, $nth, $data ) = @_; my $nick = nick( $words->[0] ); my $network = $self->get_network; my $chan = Xchat::get_info('channel'); return if !$chan; my $reason = $self->worthy( ( $nth->[2] || q{} ) . 'X', $nick, $chan, 'quit' ); return if !$reason; $reason =~ s{X$}{}xm; if ( $reason =~ /\S/xm ) { $reason = ": $reason"; } return $self->notify( who => $nick, event => 'quit', target => $chan, summary => $chan, body => "*** $nick left $network$reason", ); } sub topic_handler { my ( $self, $words, $nth, $data ) = @_; my $nick = nick( $words->[0] ); my $chan = clean( $words->[2] ); my $topic = $self->worthy( $nth->[3], $nick, $chan, 'topic' ); return if !$topic; return $self->notify( who => $nick, event => 'topic', target => $chan, summary => $chan, body => "*** $nick changed topic to: $topic", ); } sub kick_handler { my ( $self, $words, $nth, $data ) = @_; my $nick = nick( $words->[0] ); my $chan = clean( $words->[2] ); my $tnick = clean( $words->[3] ); my $reason = $self->worthy( $nth->[4], $nick, $chan, 'kick' ); return if !$reason; my $body = "*** $nick kicked $tnick"; if ( $reason ne $nick ) { $body .= ": $reason"; } return $self->notify( who => $nick, event => 'kick', target => $chan, summary => $chan, body => $body, ); } sub command_handler { my $self = shift; my ( $command, $arg ) = split /\s+/xms, shift, 2; $arg ||= q{}; for ($arg) { s/^\s+//xms; s/\s+$//xms } my $tocall = '_cmd_' . $command; if ( $self->can($tocall) ) { return $self->$tocall($arg); } else { return ( 0, 'Unknown command!' ); } } sub bold { my $value = shift; my $bold = chr 2; return $bold . $value . $bold; } sub _toggle { my $self = shift; my $key = shift || q{}; return ( 0, bold($key) . ' is not a valid configuration field.' ) if $key =~ /^__/xm || !exists $self->{$key}; $self->{$key} = $self->{$key} ? 0 : 1; my $value = $self->{$key} ? 'ON' : 'OFF'; $self->save_cf; return ( 1, bold($key) . ' messages: ' . bold($value) . q{.} ); } sub _setval { my ( $self, $key, $value ) = @_; return ( 0, bold($key) . ' is not a valid configuration field.' ) if $key =~ /^__/xm || !exists $self->{$key}; if ( !defined $value || $self->{$key} eq $value ) { return ( 1, bold($key) . ' unchanged (' . bold( $self->{$key} ) . q{)} ); } else { $self->{$key} = $value; $self->save_cf; return ( 1, 'Now ' . bold($key) . q{ = } . bold( $self->{$key} ) ); } } sub _cmd_about { my $self = shift; return ( 1, ' Name: ' . $self->package, 'Version: ' . $self->version, ' Author: ' . $self->author ); } sub _cmd_on { my $self = shift; $self->{on} = 1; $self->save_cf; return ( 1, 'Gnotify plugin turned ON.' ); } sub _cmd_off { my $self = shift; $self->{on} = 0; $self->save_cf; return ( 1, 'Gnotify plugin turned OFF.' ); } sub _cmd_ignore { my ( $self, $rex ) = @_; my $patterns = join q{|}, split /\s+/xm, $rex; $_[0]->{ignore} = qr/^(?i:$patterns)$/; $self->save_cf; return ( 1, 'Ignore pattern updated.' ); } sub _cmd_stat { my $self = shift; my @ret = (1); for my $key ( sort keys %{$self} ) { next if $key =~ /^_/xm; my $val = $self->{$key}; next if ref $val && ref $val !~ /^(ARRAY|HASH|Regexp)$/xm; CORE::push @ret, bold($key) . q{: } . $self->stringify($val); } return @ret; } sub _cmd_timeout { my ( $self, $value ) = @_; if ( !$value || $value =~ /\D/xm ) { return ( 0, 'Invalid value. Should be a number of seconds.' ); } $self->{timeout} = $value; $self->save_cf; return ( 1, bold('timeout') . q{ = } . bold($value) ); } sub _cmd_urgency { my ( $self, $value ) = @_; if ( !$value || $value !~ /^(?:low|normal|critical)$/xm ) { return ( 0, 'Invalid value. Should be one of "low", "normal" or "critical"' ); } $self->{urgency} = $value; $self->save_cf; return ( 1, bold('urgency') . q{ = } . bold($value) ); } sub _init_commands { my $class = shift; for my $field ( qw[pubmsg privmsg notice ctcp action join part quit topic kick ignore_unidentified uident] ) { $class->install_sub( { '_cmd_' . $field => sub { my $self = shift; return $self->_toggle($field); } } ); } return; } sub print_result { my ( $rc, @messages ) = @_; if (@messages) { my $prefix = $rc ? q{} : chr(2) . 'ERROR' . chr(2) . q{: }; for (@messages) { Xchat::print( $prefix . $_ . "\n" ); } } return Xchat::EAT_NONE; } sub _init { my $self = shift; $self->register; $self->set_hooks( [ 'server', 'PRIVMSG', sub { return print_result( $self->privmsg_handler(@_) ) }, ], [ 'server', 'NOTICE', sub { return print_result( $self->notice_handler(@_) ) }, ], [ 'server', 'JOIN', sub { return print_result( $self->join_handler(@_) ) }, ], [ 'server', 'PART', sub { return print_result( $self->part_handler(@_) ) }, ], [ 'server', 'QUIT', sub { return print_result( $self->quit_handler(@_) ) }, ], [ 'server', 'TOPIC', sub { return print_result( $self->topic_handler(@_) ) }, ], [ 'server', 'KICK', sub { return print_result( $self->kick_handler(@_) ) }, ], [ 'command', 'gnotify', sub { if ( !$_[1]->[1] || $_[1]->[1] =~ /^\s*help\s*/xmi || $_[1]->[1] =~ /^\s*$/xm ) { return help(); } print_result( $self->command_handler( $_[1]->[1] ) ); return Xchat::EAT_ALL; }, { help_text => 'type "/gnotify help"' } ] ); $self->loaded; } sub help { Xchat::print( [ map { $_ . "\n" } split /\n/xm, <<"EOF" ] ); Gnotify commands (/gnotify <\x02command\x02> [<\x02arg1\x02> ...]): \cC11GENERAL\cO: \x02on\x02 - turn plugin ON \x02off\x02 - turn plugin OFF \x02help\x02 - show this help message \x02about\x02 - show plugin summary \x02stat\x02 - show plugin status \cC11EVENT HANDLERS\cO: \x02pubmsg\x02 - toggle public messages \x02privmsg\x02 - toggle private messages \x02notice\x02 - toggle NOTICE \x02action\x02 - toggle ACTION \x02join\x02 - toggle JOIN \x02part\x02 - toggle PART \x02quit\x02 - toggle QUIT \x02topic\x02 - toggle TOPIC \x02kick\x02 - toggle KICK \x02ctcp\x02 - toggle CTCP \cC11NOTIFICATIONS ATTRIBUTES\cO: \x02timeout\x02 <\x02seconds\x02> - after how many seconds should notification dissapear? \x02urgency\x02 <\x02low | normal | critical\x02> - notifications urgency level \cC11IRC FEATURES\cO: \x02uident\x02 - server differentiates "identified" vs "unidentified" users? \x02ignore_unidentified\x02 - ignore unidentified users? \x02ignore\x02 <\x02pattern1\x02> <\x02pattern2\x02> ... - targets to be ignored EOF return Xchat::EAT_ALL; } 1;