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