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