# $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;