1package POE::Session::Irssi; 2use strict; 3use warnings; 4 5use POE; 6use base qw(POE::Session); 7 8use Sub::Uplevel; 9use Carp; 10use Irssi; 11 12sub import { 13 my ($class) = @_; 14 my $package = caller(); 15 16 { 17 no strict 'refs'; 18 *{ $package . '::UNLOAD' } = sub { 19 $POE::Kernel::poe_kernel->signal ( 20 $POE::Kernel::poe_kernel, 'unload', $package 21 ); 22 }; 23 } 24} 25 26#TODO - this should be in POE::Session 27sub SE_DATA () { 3 } 28 29use vars qw($VERSION); 30$VERSION = '0.50'; 31 32# local var we needn't worry about __PACKAGE__ being interpreted as 33# a string literal 34my $pkg = __PACKAGE__; 35 36=head1 NAME 37 38POE::Session::Irssi - emit POE events for Irssi signals 39 40=head1 SYNOPSIS 41 42 use Irssi; 43 use Glib; 44 use POE qw(Loop::Glib); 45 use POE::Session::Irssi; 46 47 %IRSSI = ( ... fill in the usual stuff for scripts here ... ); 48 49 POE::Session::Irssi->create ( 50 irssi_commands => { 51 hello => sub { 52 my $args = $_[ARG1]; 53 my ($data, $server, $witem) = @$args; 54 55 $server->command("MSG $witem->{name} Hello $data!"); 56 }, 57 }, 58 irssi_signals => { 59 "message join" => sub { 60 my $args = $_[ARG1]; 61 my ($server, $channel, $nick, $address) = @$args; 62 my $me = $server->{nick}; 63 64 if ($nick eq $me) { 65 $server->command("MSG $channel Hello World!"); 66 } else { 67 $server->command("MSG $channel Hi there, $nick"); 68 } 69 }, 70 }, 71 # Other create() args here.. 72 ); 73 74=head1 DESCRIPTION 75 76This L<POE::Session> subclass helps you integrate POE and Irssi scripting. 77It connects the signals and commands handlers you define as L<POE> events 78with the L<Irssi> machinery. It also tries to clean up as much as possible 79when the script gets unloaded, by removing all the alarms your session 80has running. 81 82It does this cleaning up by installing an UNLOAD handler that will send an 83unload signal. See SIGNALS below for more information. 84 85=head1 CONSTRUCTOR 86 87=head2 create (%args) 88 89Apart from the normal arguments L<POE::Session> create() supports, there 90are two more arguments. 91 92=over 2 93 94=item * 95 96irssi_commands 97 98=over 4 99 100 irssi_commands => { 101 command_name => \&handler_sub, 102 } 103 104=back 105 106As you can see in the example above, this expects a hashref, with 107the keys holding the /command you use in Irssi, and the values being 108references to the handler function. Because L<POE::Session::Irssi> 109creates a postback behind the scenes for each command, your handler 110sub will get two arguments in ARG0 and ARG1. These are the normal 111postback lists, and the arguments you would normally receive in 112an L<Irssi> handler are in the list in ARG1. 113 114Currently, only this inline_state like syntax is supported. Allowing 115for object/package states is on the TODO list. 116 117=item * 118 119irssi_signals 120 121=over 4 122 123 irssi_signals => { 124 "signal name" => \&handler_sub, 125 } 126 127=back 128 129This is much the same as for the irssi_commands. One thing to remember 130is that lots of L<Irssi> signals have spaces in their names, so don't 131forget to put them inside quotes. 132 133=back 134 135=cut 136 137# subclassing POE::Session methods to work our evil^Wmagic 138 139# here we stick our custom parameters into the newly created $self 140 141sub instantiate { 142 my ($class, $params) = @_; 143 144 my $package = caller(1); 145 my $self = $class->SUPER::instantiate; 146 147 croak "expecting a hashref" unless (ref($params) eq 'HASH'); 148 149 my $irssi_signals = delete $params->{'irssi_signals'}; 150 if (ref($irssi_signals) eq 'HASH') { 151 my %name_map = (); 152 #treat as inline states 153 $params->{inline_states} = {} unless defined $params->{inline_states}; 154 while (my ($signal, $handler) = each %$irssi_signals) { 155 my $poe_name = "_irssi_signal_$signal"; 156 $poe_name =~ s/ /_/g; 157 $name_map{$signal} = $poe_name; 158 $params->{inline_states}->{$poe_name} = $handler; 159 } 160 $self->[SE_DATA]->{$pkg}->{"signal_name_map"} = \%name_map; 161 } 162 163 my $irssi_commands = delete $params->{'irssi_commands'}; 164 if (ref($irssi_commands) eq 'HASH') { 165 my %name_map = (); 166 #treat as inline states 167 $params->{inline_states} = {} unless defined $params->{inline_states}; 168 while (my ($command, $handler) = each %$irssi_commands) { 169 my $poe_name = "_irssi_command_$command"; 170 $name_map{$command} = $poe_name; 171 $params->{inline_states}->{$poe_name} = $handler; 172 } 173 use Data::Dumper; 174 #print Dumper \%name_map; 175 $self->[SE_DATA]->{$pkg}->{"command_name_map"} = \%name_map; 176 } 177 $params->{inline_states}->{_irssi_script_unload} = sub { 178 my ($kernel, $forme) = @_[KERNEL, ARG1]; 179 180 return unless ($forme eq $package); 181 # try to clean up so that we get reaped by the kernel 182 $kernel->alarm_remove_all; 183 $kernel->sig('unload'); 184 $kernel->sig_handled(); 185 }; 186 187 return $self; 188} 189 190# Irssi wants you to call Irssi::signal_add and Irssi::command_bind 191# from the Irssi::Script::$name package it creates for your script, 192# so it can clean up. This is where we trick it into thinking we're 193# doing that. 194 195sub _connect_stuff { 196 my ($kernel, $session) = @_[KERNEL, SESSION]; 197 198 my $lvl = 1; 199 $lvl++ while (caller($lvl - 1) !~ /^Irssi::Script::/); 200 201 my $name_map = $session->[SE_DATA]->{$pkg}->{signal_name_map}; 202 while (my ($irssi_name, $poe_name) = each %$name_map) { 203 my $postback = $session->postback ($poe_name); 204 uplevel $lvl, \&Irssi::signal_add, $irssi_name, $postback; 205 } 206 $name_map = $session->[SE_DATA]->{$pkg}->{command_name_map}; 207 while (my ($irssi_name, $poe_name) = each %$name_map) { 208 my $postback = $session->postback ($poe_name); 209 uplevel $lvl, \&Irssi::command_bind, $irssi_name, $postback; 210 } 211 $kernel->sig(unload => '_irssi_script_unload'); 212} 213 214# and here we use those to set up our _start 215sub try_alloc { 216 my ($self, @start_args) = @_; 217 218 my $start_state = 219 $self->[POE::Session::SE_STATES]->{+POE::Session::EN_START}; 220 221 my $real_start_state; 222 223 # call any _start the user defined. 224 if (defined $start_state) { 225 $real_start_state = sub { 226 _connect_stuff (@_); 227 228# if (ref ($start_state) ne 'CODE') { 229# $_[OBJECT] = $object; 230# } 231 if (ref($start_state) eq 'CODE') { 232 return &$start_state (@_); 233 } else { 234 my ($clobj, $state) = @$start_state; 235 shift @_; 236 return $clobj->$state (@_); 237 } 238 }; 239 } else { 240 $real_start_state = \&_connect_stuff; 241 } 242 $self->[POE::Session::SE_STATES]->{+POE::Session::EN_START} = $real_start_state; 243 244 return $self->SUPER::try_alloc (@start_args); 245} 246 247=head1 SIGNALS 248 249POE allows you to define your own signals, which are handled the same as 250system signals. See L<POE::Kernel> for more information. 251L<POE::Session::Irssi> defines one such signal: 252 253=head2 unload $package 254 255This signal is sent when irssi tries to unload a script. ARG1 contains the 256package name of the script that is being unloaded. L<POE::Session::Irssi> 257also creates a handler for this signal that does its best to clean up for 258the session by removing any aliases set and removing the signal handler 259 260=head1 NOTES 261 262Since you don't need to call POE::Kernel->run() in Irssi scripts (because 263the L<Glib> mainloop is already running), it is no problem at all to 264have more than one Irssi script contain a L<POE::Session>. They will 265all use the same L<POE::Kernel> and L<POE::Loop>. 266 267=head1 TODO 268 269=over 2 270 271=item * 272 273Allow object/package states 274 275=item * 276 277Maybe put a list of session aliases in an Irssi setting somewhere 278This would allow discovery of what other sessions we can talk to. 279 280=back 281 282=head1 AUTHOR 283 284Martijn van Beers <martijn@eekeek.org> 285 286=head1 LICENSE gpl 287 288This module is Copyright 2006-2008 Martijn van Beers. It is free 289software; you may reproduce and/or modify it under the terms of 290the GPL version 2.0 or higher. See the file LICENSE in the source 291tarball for more information 292 293=cut 294 2951; 296