1package POE::Component::IRC::Plugin::Console;
2our $AUTHORITY = 'cpan:HINRIK';
3$POE::Component::IRC::Plugin::Console::VERSION = '6.93';
4use strict;
5use warnings FATAL => 'all';
6use Carp;
7use IRC::Utils qw(decode_irc);
8use POE qw(Wheel::SocketFactory Wheel::ReadWrite Filter::IRCD Filter::Line Filter::Stackable);
9use POE::Component::IRC::Plugin qw( :ALL );
10use Scalar::Util qw(looks_like_number);
11
12sub new {
13    my $package = shift;
14    croak "$package requires an even number of arguments" if @_ & 1;
15    my %self = @_;
16    return bless \%self, $package;
17}
18
19sub PCI_register {
20    my ($self, $irc) = splice @_, 0, 2;
21
22    $self->{irc} = $irc;
23
24    $irc->plugin_register( $self, 'SERVER', qw(all) );
25    $irc->plugin_register( $self, 'USER', qw(all) );
26
27    POE::Session->create(
28        object_states => [
29            $self => [ qw(_client_error _client_flush _client_input _listener_accept _listener_failed _start _shutdown) ],
30        ],
31    );
32
33    return 1;
34}
35
36sub PCI_unregister {
37    my ($self, $irc) = splice @_, 0, 2;
38
39    delete $self->{irc};
40    $poe_kernel->post( $self->{SESSION_ID} => '_shutdown' );
41    $poe_kernel->refcount_decrement( $self->{SESSION_ID}, __PACKAGE__ );
42    return 1;
43}
44
45sub _dump {
46    my ($arg) = @_;
47
48    if (ref $arg eq 'ARRAY') {
49        my @elems;
50        for my $elem (@$arg) {
51            push @elems, _dump($elem);
52        }
53        return '['. join(', ', @elems) .']';
54    }
55    elsif (ref $arg eq 'HASH') {
56        my @pairs;
57        for my $key (keys %$arg) {
58            push @pairs, [$key, _dump($arg->{$key})];
59        }
60        return '{'. join(', ', map { "$_->[0] => $_->[1]" } @pairs) .'}';
61    }
62    elsif (ref $arg) {
63        require overload;
64        return overload::StrVal($arg);
65    }
66    elsif (defined $arg) {
67        return $arg if looks_like_number($arg);
68        return "'".decode_irc($arg)."'";
69    }
70    else {
71        return 'undef';
72    }
73}
74
75sub _default {
76    my ($self, $irc, $event) = splice @_, 0, 3;
77    return PCI_EAT_NONE if $event eq 'S_raw';
78
79    pop @_;
80    my @args = map { $$_ } @_;
81    my @output;
82
83    for my $i (0..$#args) {
84        push @output, "ARG$i: " . _dump($args[$i]);
85    }
86
87    for my $wheel_id ( keys %{ $self->{wheels} } ) {
88        next if ( $self->{exit}->{ $wheel_id } or ( not defined ( $self->{wheels}->{ $wheel_id } ) ) );
89        next if !$self->{authed}{ $wheel_id };
90        $self->{wheels}->{ $wheel_id }->put("$event: ".join(', ', @output));
91    }
92
93    return PCI_EAT_NONE;
94}
95
96sub _start {
97    my ($kernel, $self) = @_[KERNEL, OBJECT];
98
99    $self->{SESSION_ID} = $_[SESSION]->ID();
100    $kernel->refcount_increment( $self->{SESSION_ID}, __PACKAGE__ );
101    $self->{ircd_filter} = POE::Filter::Stackable->new( Filters => [
102        POE::Filter::Line->new(),
103        POE::Filter::IRCD->new(),
104    ]);
105
106    $self->{listener} = POE::Wheel::SocketFactory->new(
107        BindAddress  => 'localhost',
108        BindPort     => $self->{bindport} || 0,
109        SuccessEvent => '_listener_accept',
110        FailureEvent => '_listener_failed',
111        Reuse        => 'yes',
112    );
113
114    if ($self->{listener}) {
115        $self->{irc}->send_event( 'irc_console_service' => $self->{listener}->getsockname() );
116    }
117    else {
118        $self->{irc}->plugin_del( $self );
119    }
120
121    return;
122}
123
124sub _listener_accept {
125    my ($kernel, $self, $socket, $peeradr, $peerport)
126        = @_[KERNEL, OBJECT, ARG0 .. ARG2];
127
128    my $wheel = POE::Wheel::ReadWrite->new(
129        Handle       => $socket,
130        InputFilter  => $self->{ircd_filter},
131        OutputFilter => POE::Filter::Line->new(),
132        InputEvent   => '_client_input',
133        ErrorEvent   => '_client_error',
134        FlushedEvent => '_client_flush',
135    );
136
137    if ( !defined $wheel ) {
138        $self->{irc}->send_event( 'irc_console_rw_fail' => $peeradr => $peerport );
139        return;
140    }
141
142    my $wheel_id = $wheel->ID();
143    $self->{wheels}->{ $wheel_id } = $wheel;
144    $self->{authed}->{ $wheel_id } = 0;
145    $self->{exit}->{ $wheel_id } = 0;
146    $self->{irc}->send_event( 'irc_console_connect' => $peeradr => $peerport => $wheel_id );
147
148    return;
149}
150
151sub _listener_failed {
152    delete $_[OBJECT]->{listener};
153    return;
154}
155
156sub _client_input {
157    my ($kernel, $self, $input, $wheel_id) = @_[KERNEL, OBJECT, ARG0, ARG1];
158
159    if ($self->{authed}->{ $wheel_id } && lc ( $input->{command} ) eq 'exit') {
160        $self->{exit}->{ $wheel_id } = 1;
161        if (defined $self->{wheels}->{ $wheel_id }) {
162            $self->{wheels}->{ $wheel_id }->put("ERROR * quiting *");
163        }
164        return;
165    }
166
167    if ( $self->{authed}->{ $wheel_id } ) {
168        $self->{irc}->yield( lc ( $input->{command} ) => @{ $input->{params} } );
169        return;
170    }
171
172    if (lc ( $input->{command} ) eq 'pass' && $input->{params}->[0] eq $self->{password} ) {
173        $self->{authed}->{ $wheel_id } = 1;
174        $self->{wheels}->{ $wheel_id }->put('NOTICE * Password accepted *');
175        $self->{irc}->send_event( 'irc_console_authed' => $wheel_id );
176        return;
177    }
178
179    $self->{wheels}->{ $wheel_id }->put('NOTICE * Password required * enter PASS <password> *');
180    return;
181}
182
183sub _client_flush {
184    my ($self, $wheel_id) = @_[OBJECT, ARG0];
185    return if !$self->{exit}->{ $wheel_id };
186    delete $self->{wheels}->{ $wheel_id };
187    return;
188}
189
190sub _client_error {
191    my ($self, $wheel_id) = @_[OBJECT, ARG3];
192
193    delete $self->{wheels}->{ $wheel_id };
194    delete $self->{authed}->{ $wheel_id };
195    $self->{irc}->send_event( 'irc_console_close' => $wheel_id );
196    return;
197}
198
199sub _shutdown {
200    my ($kernel, $self) = @_[KERNEL, OBJECT];
201
202    delete $self->{listener};
203    delete $self->{wheels};
204    delete $self->{authed};
205    return;
206}
207
208sub getsockname {
209    my $self = shift;
210    return if !$self->{listener};
211    return $self->{listener}->getsockname();
212}
213
2141;
215
216=encoding utf8
217
218=head1 NAME
219
220POE::Component::IRC::Plugin::Console - A PoCo-IRC plugin that provides a
221lightweight debugging and control console for your bot
222
223=head1 SYNOPSIS
224
225 use POE qw(Component::IRC Component::IRC::Plugin::Console);
226
227 my $nickname = 'Flibble' . $$;
228 my $ircname = 'Flibble the Sailor Bot';
229 my $ircserver = 'irc.blahblahblah.irc';
230 my $port = 6667;
231 my $bindport = 6969;
232
233 my @channels = ( '#Blah', '#Foo', '#Bar' );
234
235 my $irc = POE::Component::IRC->spawn(
236     nick => $nickname,
237     server => $ircserver,
238     port => $port,
239     ircname => $ircname,
240 ) or die "Oh noooo! $!";
241
242 POE::Session->create(
243     package_states => [
244         main => [ qw(_start irc_001 irc_console_service irc_console_connect
245             irc_console_authed irc_console_close irc_console_rw_fail) ],
246         ],
247 );
248
249 $poe_kernel->run();
250
251 sub _start {
252     $irc->plugin_add( 'Console' => POE::Component::IRC::Plugin::Console->new(
253         bindport => $bindport,
254         password => 'opensesame'
255     );
256     $irc->yield( register => 'all' );
257     $irc->yield( connect => { } );
258     return;
259  }
260
261 sub irc_001 {
262     $irc->yield( join => $_ ) for @channels;
263     return;
264 }
265
266 sub irc_console_service {
267     my $getsockname = $_[ARG0];
268     return;
269 }
270
271 sub irc_console_connect {
272     my ($peeradr, $peerport, $wheel_id) = @_[ARG0 .. ARG2];
273     return;
274 }
275
276 sub irc_console_authed {
277     my $wheel_id = $_[ARG0];
278     return;
279 }
280
281 sub irc_console_close {
282     my $wheel_id = $_[ARG0];
283     return;
284 }
285
286 sub irc_console_rw_fail {
287     my ($peeradr, $peerport) = @_[ARG0, ARG1];
288     return;
289 }
290
291=head1 DESCRIPTION
292
293POE::Component::IRC::Plugin::Console is a L<POE::Component::IRC|POE::Component::IRC>
294plugin that provides an interactive console running over the loopback network.
295One connects to the listening socket using a telnet client (or equivalent),
296authenticate using the applicable password. Once authed one will receive all
297events that are processed through the component. One may also issue all the
298documented component commands.
299
300=head1 METHODS
301
302=head2 C<new>
303
304Takes two arguments:
305
306B<'password'>, the password to set for *all* console connections;
307
308B<'bindport'>, specify a particular port to bind to, defaults to 0, ie. randomly
309allocated;
310
311Returns a plugin object suitable for feeding to
312L<POE::Component::IRC|POE::Component::IRC>'s C<plugin_add> method.
313
314=head2 C<getsockname>
315
316Gives access to the underlying listener's C<getsockname> method. See
317L<POE::Wheel::SocketFactory|POE::Wheel::SocketFactory> for details.
318
319=head1 OUTPUT EVENTS
320
321The plugin generates the following additional
322L<POE::Component::IRC|POE::Component::IRC> events:
323
324=head2 C<irc_console_service>
325
326Emitted when a listener is successfully spawned. C<ARG0> is the result of
327C<getsockname>, see above for details.
328
329=head2 C<irc_console_connect>
330
331Emitted when a client connects to the console. C<ARG0> is the peeradr, C<ARG1>
332is the peer port and C<ARG2> is the wheel id of the connection.
333
334=head2 C<irc_console_authed>
335
336Emitted when a client has successfully provided a valid password. C<ARG0> is
337the wheel id of the connection.
338
339=head2 C<irc_console_close>
340
341Emitted when a client terminates a connection. C<ARG0> is the wheel id of the
342connection.
343
344=head2 C<irc_console_rw_fail>
345
346Emitted when a L<POE::Wheel::ReadWrite|POE::Wheel::ReadWrite> could not be
347created on a socket. C<ARG0> is the peer's address, C<ARG1> is the peer's port.
348
349=head1 AUTHOR
350
351Chris 'BinGOs' Williams
352
353=head1 SEE ALSO
354
355L<POE::Component::IRC|POE::Component::IRC>
356
357L<POE::Wheel::SocketFactory|POE::Wheel::SocketFactory>
358
359=cut
360