File Coverage

blib/lib/App/Pocoirc.pm
Criterion Covered Total %
statement 8 10 80.0
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 12 14 85.7


line stmt bran cond sub pod time code
1             package App::Pocoirc;
2             BEGIN {
3 1     1   3330 $App::Pocoirc::AUTHORITY = 'cpan:HINRIK';
4             }
5             {
6             $App::Pocoirc::VERSION = '0.47';
7             }
8              
9 1     1   8 use strict;
  1         2  
  1         42  
10 1     1   5 use warnings FATAL => 'all';
  1         2  
  1         45  
11              
12 1     1   50 use App::Pocoirc::Status;
  0            
  0            
13             use Class::Load qw(try_load_class);
14             use Fcntl qw(O_CREAT O_EXCL O_WRONLY);
15             use File::Glob ':glob';
16             use File::Spec::Functions 'rel2abs';
17             use IO::Handle;
18             use IRC::Utils qw(decode_irc);
19             use POE;
20             use POE::Component::Client::DNS;
21             use POSIX 'strftime';
22             use Scalar::Util 'looks_like_number';
23              
24             sub new {
25             my ($package, %args) = @_;
26             return bless \%args, $package;
27             }
28              
29             sub run {
30             my ($self) = @_;
31              
32             # we print IRC output, which will be UTF-8
33             binmode $_, ':utf8' for (*STDOUT, *STDERR);
34              
35             if ($self->{list_plugins}) {
36             require Module::Pluggable;
37             Module::Pluggable->import(
38             sub_name => '_available_plugins',
39             search_path => 'POE::Component::IRC::Plugin',
40             );
41             for my $plugin (sort $self->_available_plugins()) {
42             $plugin =~ s/^POE::Component::IRC::Plugin:://;
43             print $plugin, "\n";
44             }
45             return;
46             }
47              
48             $self->_setup();
49              
50             if ($self->{check_cfg}) {
51             print "The configuration is valid and all modules could be compiled.\n";
52             return;
53             }
54              
55             if ($self->{daemonize}) {
56             require Proc::Daemon;
57             eval {
58             Proc::Daemon::Init->();
59             if (defined $self->{log_file}) {
60             open STDOUT, '>>:encoding(utf8)', $self->{log_file}
61             or die "Can't open $self->{log_file}: $!\n";
62             open STDERR, '>>&STDOUT' or die "Can't redirect STDERR: $!\n";
63             STDOUT->autoflush(1);
64             }
65             $poe_kernel->has_forked();
66             };
67             chomp $@;
68             die "Can't daemonize: $@\n" if $@;
69             }
70              
71             if (defined $self->{pid_file}) {
72             sysopen my $fh, $self->{pid_file}, O_CREAT|O_EXCL|O_WRONLY
73             or die "Can't create pid file or it already exists. Pocoirc already running?\n";
74             print $fh "$$\n";
75             close $fh;
76             }
77              
78             POE::Session->create(
79             object_states => [
80             $self => [qw(
81             _start
82             sig_die
83             sig_int
84             sig_term
85             irc_plugin_add
86             irc_plugin_del
87             irc_plugin_error
88             irc_plugin_status
89             irc_network
90             irc_shutdown
91             )],
92             $self => {
93             irc_432 => 'irc_432_or_433',
94             irc_433 => 'irc_432_or_433',
95             },
96             ],
97             );
98              
99             $poe_kernel->run();
100             unlink $self->{pid_file} if defined $self->{pid_file};
101             return;
102             }
103              
104             sub _setup {
105             my ($self) = @_;
106              
107             if (defined $self->{cfg}{pid_file}) {
108             $self->{pid_file} = rel2abs(bsd_glob(delete $self->{cfg}{pid_file}));
109             }
110              
111             if (defined $self->{cfg}{log_file}) {
112             my $log = rel2abs(bsd_glob(delete $self->{cfg}{log_file}));
113             open my $fh, '>>', $log or die "Can't open $log: $!\n";
114             close $fh;
115             $self->{log_file} = $log;
116             }
117              
118             if (!$self->{no_color}) {
119             require Term::ANSIColor;
120             Term::ANSIColor->import();
121             }
122              
123             if (defined $self->{cfg}{lib}) {
124             if (ref $self->{cfg}{lib} eq 'ARRAY' && @{ $self->{cfg}{lib} }) {
125             unshift @INC, map { rel2abs(bsd_glob($_)) } @{ delete $self->{cfg}{lib} };
126             }
127             else {
128             unshift @INC, rel2abs(bsd_glob(delete $self->{cfg}{lib}));
129             }
130             }
131              
132             $self->_load_classes();
133             return;
134             }
135              
136             sub _load_classes {
137             my ($self) = @_;
138              
139             for my $plug_spec (@{ $self->{cfg}{global_plugins} || [] }) {
140             $self->_load_plugin($plug_spec);
141             }
142              
143             while (my ($network, $opts) = each %{ $self->{cfg}{networks} }) {
144             while (my ($opt, $value) = each %{ $self->{cfg} }) {
145             next if $opt =~ /^(?:networks|global_plugins|local_plugins)$/;
146             $opts->{$opt} = $value if !defined $opts->{$opt};
147             }
148              
149             for my $plug_spec (@{ $opts->{local_plugins} || [] }) {
150             $self->_load_plugin($plug_spec);
151             }
152              
153             if (!defined $opts->{server}) {
154             die "Server for network '$network' not specified\n";
155             }
156              
157             if (defined $opts->{class}) {
158             $opts->{class} = _load_either_class(
159             "POE::Component::IRC::$opts->{class}",
160             $opts->{class},
161             );
162             }
163             else {
164             $opts->{class} = 'POE::Component::IRC::State';
165             my ($success, $error) = try_load_class($opts->{class});
166             chomp $error if defined $error;
167             die "Can't load class $opts->{class}: $error\n" if !$success;
168             }
169             }
170              
171             return;
172             }
173              
174             # create plugins, spawn components, and connect to IRC
175             sub _start {
176             my ($kernel, $session, $self) = @_[KERNEL, SESSION, OBJECT];
177              
178             $kernel->sig(DIE => 'sig_die');
179             $kernel->sig(INT => 'sig_int');
180             $kernel->sig(TERM => 'sig_term');
181              
182             $self->_status(undef, 'normal', "Started (pid $$)");
183             my ($own, $global, $local) = $self->_construct_objects();
184             $self->_register_plugins($session->ID(), $own, $global, $local);
185             $self->{own_plugins} = $own;
186              
187             for my $entry (@{ $self->{ircs} }) {
188             my ($network, $irc) = @$entry;
189             $self->_status($network, 'normal', 'Connecting to IRC ('.$irc->server.')');
190             $irc->yield('connect');
191             }
192              
193             return;
194             }
195              
196             sub _construct_objects {
197             my ($self) = @_;
198              
199             # create the shared DNS resolver
200             $self->{resolver} = POE::Component::Client::DNS->spawn();
201              
202             # construct global plugins
203             $self->_status(undef, 'normal', "Constructing global plugins");
204              
205             my $global_plugs = $self->_create_plugins(delete $self->{cfg}{global_plugins});
206              
207             my $own_plugs = [
208             [
209             'PocoircStatus',
210             App::Pocoirc::Status->new(
211             Pocoirc => $self,
212             Trace => $self->{trace},
213             Verbose => $self->{verbose},
214             Dynamic => (defined $self->{cfg_file} ? 1 : 0),
215             ),
216             ],
217             ];
218              
219             if ($self->{interactive}) {
220             require App::Pocoirc::ReadLine;
221             push @$own_plugs, [
222             'PocoircReadLine',
223             App::Pocoirc::ReadLine->new(
224             Pocoirc => $self,
225             ),
226             ];
227             }
228              
229             my $local_plugs;
230             # construct IRC components
231             while (my ($network, $opts) = each %{ $self->{cfg}{networks} }) {
232             my $class = delete $opts->{class};
233              
234             # construct network-specific plugins
235             $self->_status($network, 'normal', 'Constructing local plugins');
236             $local_plugs->{$network} = $self->_create_plugins(delete $opts->{local_plugins});
237              
238             $self->_status($network, 'normal', "Spawning IRC component ($class)");
239             my $irc = $class->spawn(
240             %$opts,
241             Resolver => $self->{resolver},
242             );
243             my $isa = eval { $irc->isa($class) };
244             die "isa() test failed for component of class $class\n" if !$isa;
245             push @{ $self->{ircs} }, [$network, $irc];
246             }
247              
248             return $own_plugs, $global_plugs, $local_plugs;
249             }
250              
251             sub _load_either_class {
252             my ($primary, $secondary) = @_;
253              
254             my ($success, $error, @err);
255             ($success, $error) = try_load_class($primary);
256             return $primary if $success;
257              
258             push @err, $error;
259             ($success, $error) = try_load_class($secondary);
260             return $secondary if $success;
261              
262             chomp $error if defined $error;
263             push @err, $error;
264              
265             my $class = "$primary or $secondary";
266             if (@err == 2) {
267             if ($err[0] =~ /^Can't locate / && $err[1] !~ /^Can't locate /) {
268             $class = $secondary;
269             shift @err;
270             }
271             elsif ($err[1] =~ /^Can't locate / && $err[0] !~ /^Can't locate /) {
272             $class = $primary;
273             pop @err;
274             }
275             }
276             my $reason = join "\n", map { " $_" } @err;
277             die "Failed to load class $class:\n$reason\n";
278             }
279              
280             sub _register_plugins {
281             my ($self, $session_id, $own, $global, $local) = @_;
282              
283             for my $entry (@{ $self->{ircs} }) {
284             my ($network, $irc) = @$entry;
285             $self->_status($network, 'normal', 'Registering plugins');
286              
287             for my $plugin (@$own, @$global, @{ $local->{$network} }) {
288             my ($name, $object) = @$plugin;
289             $irc->plugin_add("${name}_$session_id", $object,
290             network => $network,
291             );
292             }
293             }
294              
295             return;
296             }
297              
298             sub _dump {
299             my ($arg) = @_;
300              
301             if (ref $arg eq 'ARRAY') {
302             my @elems;
303             for my $elem (@$arg) {
304             push @elems, _dump($elem);
305             }
306             return '['. join(', ', @elems) .']';
307             }
308             elsif (ref $arg eq 'HASH') {
309             my @pairs;
310             for my $key (keys %$arg) {
311             push @pairs, [$key, _dump($arg->{$key})];
312             }
313             return '{'. join(', ', map { "$_->[0] => $_->[1]" } @pairs) .'}';
314             }
315             elsif (ref $arg) {
316             require overload;
317             return overload::StrVal($arg);
318             }
319             elsif (defined $arg) {
320             return $arg if looks_like_number($arg);
321             return "'".decode_irc($arg)."'";
322             }
323             else {
324             return 'undef';
325             }
326             }
327              
328             sub _event_debug {
329             my ($self, $irc, $args, $event) = @_;
330              
331             if (!defined $event) {
332             $event = (caller(1))[3];
333             $event =~ s/.*:://;
334             }
335              
336             my @output;
337             for my $i (0..$#{ $args }) {
338             push @output, "ARG$i: " . _dump($args->[$i]);
339             }
340             $self->_status($irc, 'debug', "$event: ".join(', ', @output));
341             return;
342             }
343              
344             # let's log this if it's preventing us from logging in
345             sub irc_432_or_433 {
346             my $self = $_[OBJECT];
347             my $irc = $_[SENDER]->get_heap();
348             my $reason = decode_irc($_[ARG2]->[1]);
349             return if $irc->logged_in();
350             my $nick = $irc->nick_name();
351             $self->_status($irc, 'normal', "Login attempt failed: $reason");
352             return;
353             }
354              
355             # fetch the server name if we're not using a config file
356             sub irc_network {
357             my ($self, $sender, $network) = @_[OBJECT, SENDER, ARG0];
358             my $irc = $sender->get_heap();
359              
360             for my $idx (0..$#{ $self->{ircs} }) {
361             if ($self->{ircs}[$idx][1] == $irc) {
362             $self->{ircs}[$idx][0] = $network;
363             last;
364             }
365             }
366             return;
367             }
368              
369             # we handle plugin status messages here because the status plugin won't
370             # see these for previously added plugins or plugin_del for itself, etc
371             sub irc_plugin_add {
372             my ($self, $alias) = @_[OBJECT, ARG0];
373             my $irc = $_[SENDER]->get_heap();
374             $self->_event_debug($irc, [@_[ARG0..$#_]], 'S_plugin_add') if $self->{trace};
375             $self->_status($irc, 'normal', "Added plugin $alias");
376             return;
377             }
378              
379             sub irc_plugin_del {
380             my ($self, $alias) = @_[OBJECT, ARG0];
381             my $irc = $_[SENDER]->get_heap();
382             $self->_event_debug($irc, [@_[ARG0..$#_]], 'S_plugin_del') if $self->{trace};
383             $self->_status($irc, 'normal', "Deleted plugin $alias");
384             return;
385             }
386              
387             sub irc_plugin_error {
388             my ($self, $error) = @_[OBJECT, ARG0];
389             my $irc = $_[SENDER]->get_heap();
390             $self->_event_debug($irc, [@_[ARG0..$#_]], 'S_plugin_error') if $self->{trace};
391             $self->_status($irc, 'error', $error);
392             return;
393             }
394              
395             sub irc_plugin_status {
396             my ($self, $plugin, @args) = @_[OBJECT, ARG0..$#_];
397             my $irc = $_[SENDER]->get_heap();
398             my $plugins = $irc->plugin_list();
399             my %plug2alias = map { $plugins->{$_} => $_ } keys %$plugins;
400              
401             my $extension = ref $plugin eq 'App::Pocoirc::Status'
402             ? ''
403             : "/$plug2alias{$plugin}";
404             $self->_status($self->_irc_to_network($irc).$extension, @args);
405             return;
406             }
407              
408             sub irc_shutdown {
409             my ($self) = $_[OBJECT];
410             my $irc = $_[SENDER]->get_heap();
411             $self->_event_debug($irc, [@_[ARG0..$#_]], 'S_shutdown') if $self->{trace};
412             $self->_status($irc, 'normal', 'IRC component shut down');
413             return;
414             }
415              
416             sub verbose {
417             my ($self, $value) = @_;
418             if (defined $value) {
419             $self->{verbose} = $value;
420             for my $plugin (@{ $self->{own_plugins} }) {
421             $plugin->[1]->verbose($value) if $plugin->[1]->can('verbose');
422             }
423             }
424             return $self->{verbose};
425             }
426              
427             sub trace {
428             my ($self, $value) = @_;
429             if (defined $value) {
430             $self->{trace} = $value;
431             for my $plugin (@{ $self->{own_plugins} }) {
432             $plugin->[1]->trace($value) if $plugin->[1]->can('trace');
433             }
434             }
435             return $self->{trace};
436             }
437              
438             sub _status {
439             my ($self, $context, $type, $message) = @_;
440              
441             my $stamp = strftime('%Y-%m-%d %H:%M:%S', localtime);
442             my $irc = eval { $context->isa('POE::Component::IRC') };
443             $context = $self->_irc_to_network($context) if $irc;
444             $context = defined $context ? " [$context]\t" : ' ';
445              
446             if (defined $type && $type eq 'error') {
447             $message = "!!! $message";
448             }
449              
450             my $log_line = "$stamp$context$message";
451             my $term_line = $log_line;
452              
453             if (!$self->{no_color}) {
454             if (defined $type && $type eq 'error') {
455             $term_line = colored($term_line, 'red');
456             }
457             elsif (defined $type && $type eq 'debug') {
458             $term_line = colored($term_line, 'yellow');
459             }
460             else {
461             $term_line = colored($term_line, 'green');
462             }
463             }
464              
465             print $term_line, "\n" if !$self->{daemonize};
466             if (defined $self->{log_file}) {
467             if (open my $fh, '>>:encoding(utf8)', $self->{log_file}) {
468             $fh->autoflush(1);
469             print $fh $log_line, "\n";
470             close $fh;
471             }
472             elsif (!$self->{daemonize}) {
473             warn "Can't open $self->{log_file}: $!\n";
474             }
475             }
476             return;
477             }
478              
479             sub _irc_to_network {
480             my ($self, $irc) = @_;
481              
482             for my $entry (@{ $self->{ircs} }) {
483             my ($network, $object) = @$entry;
484             return $network if $irc == $object;
485             }
486              
487             return;
488             }
489              
490             # find out the canonical class name for the plugin and load it
491             sub _load_plugin {
492             my ($self, $plug_spec) = @_;
493              
494             return if defined $plug_spec->[2];
495             my ($class, $args) = @$plug_spec;
496             $args = {} if !defined $args;
497              
498             my $canonclass = _load_either_class(
499             "POE::Component::IRC::Plugin::$class",
500             $class,
501             );
502              
503             $plug_spec->[1] = $args;
504             $plug_spec->[2] = $canonclass;
505             return;
506             }
507              
508             sub _create_plugins {
509             my ($self, $plugins) = @_;
510              
511             my @return;
512             for my $plug_spec (@$plugins) {
513             my ($class, $args, $canonclass) = @$plug_spec;
514             my $obj = $canonclass->new(%$args);
515             my $isa = eval { $obj->isa($canonclass) };
516             die "isa() test failed for plugin of class $canonclass\n" if !$isa;
517             push @return, [$class, $obj];
518             }
519              
520             return \@return;
521             }
522              
523             sub sig_die {
524             my ($kernel, $self, $ex) = @_[KERNEL, OBJECT, ARG1];
525             chomp $ex->{error_str};
526              
527             my $error = "Event $ex->{event} in session ".$ex->{dest_session}->ID
528             ." raised exception:\n $ex->{error_str}";
529              
530             $self->_status(undef, 'error', $error);
531             $self->shutdown('Exiting due to an exception') if !$self->{shutdown};
532             $kernel->sig_handled();
533             return;
534             }
535              
536             sub sig_int {
537             my ($kernel, $self) = @_[KERNEL, OBJECT];
538             $self->shutdown('Exiting due to SIGINT') if !$self->{shutdown};
539             $kernel->sig_handled();
540             return;
541             }
542              
543             sub sig_term {
544             my ($kernel, $self) = @_[KERNEL, OBJECT];
545              
546             $self->shutdown('Exiting due to SIGTERM') if !$self->{shutdown};
547             $kernel->sig_handled();
548             return;
549             }
550              
551             sub shutdown {
552             my ($self, $reason) = @_;
553              
554             $self->_status(undef, 'normal', $reason);
555              
556             my $logged_in;
557             for my $irc (@{ $self->{ircs} }) {
558             my ($network, $obj) = @$irc;
559              
560             if (!$logged_in && $obj->logged_in()) {
561             $logged_in = 1;
562             $self->_status(undef, 'normal',
563             'Waiting up to 5 seconds for IRC server(s) to disconnect us');
564             }
565             $obj->yield('shutdown', $reason, 5);
566             }
567              
568             $self->{resolver}->shutdown() if $self->{resolver};
569             $self->{shutdown} = 1;
570             return;
571             }
572              
573             1;
574              
575             =encoding utf8
576              
577             =head1 NAME
578              
579             App::Pocoirc - A command line tool for launching POE::Component::IRC clients
580              
581             =head1 DESCRIPTION
582              
583             This distribution provides a generic way to launch IRC clients which use
584             L. The main features are:
585              
586             =over 4
587              
588             =item * Prints useful status information (to your terminal and/or a log file)
589              
590             =item * Will daemonize if you so wish
591              
592             =item * Supports a configuration file
593              
594             =item * Offers a user friendly way to pass arguments to POE::Component::IRC
595              
596             =item * Supports multiple IRC components and lets you specify which plugins
597             to load locally (one object per component) or globally (single object)
598              
599             =item * Has an interactive mode where you can issue issue commands and
600             call methods on the IRC component(s).
601              
602             It can be used to launch IRC bots or proxies, loaded with plugins of your
603             choice. It is very useful for testing and debugging
604             L plugins as well as IRC servers.
605              
606             =back
607              
608             =head1 CONFIGURATION
609              
610             nick: foobar1234
611             username: foobar
612             log_file: /my/log.file
613             lib: /my/modules
614              
615             global_plugins:
616             - [CTCP]
617              
618             local_plugins:
619             - [BotTraffic]
620              
621             networks:
622             freenode:
623             server: irc.freenode.net
624             local_plugins:
625             - [AutoJoin, { Channels: ['#foodsfdsf'] } ]
626             magnet:
627             server: irc.perl.org
628             nick: hlagherf32fr
629              
630             The configuration file is in L or L format. It consists
631             of a hash containing C, C, C, C,
632             C, C, and default parameters to
633             L. Only C is
634             required.
635              
636             C is either the name of a directory containing Perl modules (e.g.
637             plugins), or an array of such names. Kind of like Perl's I<-I>.
638              
639             C is the path to a log file to which status messages will be written.
640              
641             C is the IRC component class. Defaults to
642             L.
643              
644             =head2 Networks
645              
646             The C option should be a hash of network hashes. The keys are the
647             names of the networks. A network hash can contain C and
648             parameters to POE::Component::IRC. None are required, except C if not
649             defined at the top level. The POE::Component::IRC parameters specified in this
650             hash will override the ones specified at the top level.
651              
652             =head2 Plugins
653              
654             The C and C options should consist of an array
655             containing the short plugin class name (e.g. 'AutoJoin') and optionally a hash
656             of arguments to that plugin. When figuring out the correct package name,
657             App::Pocoirc will first try to load POE::Component::IRC::Plugin::I
658             before trying to load I.
659              
660             The plugins in C will be instantiated once and then added to
661             all IRC components. B not all plugins are designed to be used with
662             multiple IRC components simultaneously.
663              
664             If you specify C at the top level, it will serve as a default
665             list of local plugins, which can be overridden in a network hash.
666              
667             =head1 OUTPUT
668              
669             Here is some example output from the program:
670              
671             $ pocoirc -f example/config.yml
672             2011-04-18 18:10:52 Started (pid 20105)
673             2011-04-18 18:10:52 Constructing global plugins
674             2011-04-18 18:10:52 [freenode] Constructing local plugins
675             2011-04-18 18:10:52 [freenode] Spawning IRC component (POE::Component::IRC::State)
676             2011-04-18 18:10:52 [magnet] Constructing local plugins
677             2011-04-18 18:10:52 [magnet] Spawning IRC component (POE::Component::IRC::State)
678             2011-04-18 18:10:52 [freenode] Registering plugins
679             2011-04-18 18:10:52 [magnet] Registering plugins
680             2011-04-18 18:10:52 [freenode] Connecting to IRC (irc.freenode.net)
681             2011-04-18 18:10:52 [magnet] Connecting to IRC (irc.perl.org)
682             2011-04-18 18:10:52 [freenode] Added plugin Whois3
683             2011-04-18 18:10:52 [freenode] Added plugin ISupport3
684             2011-04-18 18:10:52 [freenode] Added plugin DCC3
685             2011-04-18 18:10:52 [magnet] Added plugin Whois5
686             2011-04-18 18:10:52 [magnet] Added plugin ISupport5
687             2011-04-18 18:10:52 [magnet] Added plugin DCC5
688             2011-04-18 18:10:52 [freenode] Added plugin CTCP1
689             2011-04-18 18:10:52 [freenode] Added plugin AutoJoin1
690             2011-04-18 18:10:52 [freenode] Added plugin PocoircStatus1
691             2011-04-18 18:10:52 [magnet] Added plugin CTCP1
692             2011-04-18 18:10:52 [magnet] Added plugin PocoircStatus1
693             2011-04-18 18:10:52 [magnet] Connected to server irc.perl.org
694             2011-04-18 18:10:52 [magnet] Server notice: *** Looking up your hostname...
695             2011-04-18 18:10:52 [magnet] Server notice: *** Checking Ident
696             2011-04-18 18:10:52 [freenode] Connected to server irc.freenode.net
697             2011-04-18 18:10:53 [magnet] Server notice: *** Found your hostname
698             2011-04-18 18:10:53 [freenode] Server notice: *** Looking up your hostname...
699             2011-04-18 18:10:53 [freenode] Server notice: *** Checking Ident
700             2011-04-18 18:10:53 [freenode] Server notice: *** Couldn't look up your hostname
701             2011-04-18 18:11:03 [magnet] Server notice: *** No Ident response
702             2011-04-18 18:11:03 [magnet] Logged in to server magnet.shadowcat.co.uk with nick hlagherf32fr
703             2011-04-18 18:11:07 [freenode] Server notice: *** No Ident response
704             2011-04-18 18:11:07 [freenode] Logged in to server niven.freenode.net with nick foobar1234
705             2011-04-18 18:11:11 [freenode] Joined channel #foodsfdsf
706             ^C2011-04-18 18:11:22 Exiting due to SIGINT
707             2011-04-18 18:11:22 Waiting up to 5 seconds for IRC server(s) to disconnect us
708             2011-04-18 18:11:22 [magnet] Error from IRC server: Closing Link: 212-30-192-157.static.simnet.is ()
709             2011-04-18 18:11:22 [magnet] Deleted plugin DCC5
710             2011-04-18 18:11:22 [magnet] Deleted plugin ISupport5
711             2011-04-18 18:11:22 [magnet] Deleted plugin CTCP1
712             2011-04-18 18:11:22 [magnet] Deleted plugin Whois5
713             2011-04-18 18:11:22 [magnet] Deleted plugin PocoircStatus1
714             2011-04-18 18:11:22 [magnet] IRC component shut down
715             2011-04-18 18:11:22 [freenode] Quit from IRC (Client Quit)
716             2011-04-18 18:11:22 [freenode] Error from IRC server: Closing Link: 212.30.192.157 (Client Quit)
717             2011-04-18 18:11:22 [freenode] Deleted plugin AutoJoin1
718             2011-04-18 18:11:22 [freenode] Deleted plugin CTCP1
719             2011-04-18 18:11:22 [freenode] Deleted plugin DCC3
720             2011-04-18 18:11:22 [freenode] Deleted plugin PocoircStatus1
721             2011-04-18 18:11:22 [freenode] Deleted plugin Whois3
722             2011-04-18 18:11:22 [freenode] Deleted plugin ISupport3
723             2011-04-18 18:11:22 [freenode] IRC component shut down
724              
725             =head1 AUTHOR
726              
727             Hinrik Ern SigurEsson, hinrik.sig@gmail.com
728              
729             =head1 LICENSE AND COPYRIGHT
730              
731             Copyright 2010 Hinrik Ern SigurEsson
732              
733             This program is free software, you can redistribute it and/or modify
734             it under the same terms as Perl itself.
735              
736             =cut