root/trunk/lib/whatbot/Controller.pm

Revision 230, 14.5 KB (checked in by oz, 3 months ago)

Herp the derp derp

  • Property svn:executable set to *
Line 
1###########################################################################
2# whatbot/Controller.pm
3###########################################################################
4# Handles incoming messages and where they go
5###########################################################################
6# the whatbot project - http://www.whatbot.org
7###########################################################################
8
9use MooseX::Declare;
10
11class whatbot::Controller extends whatbot::Component with whatbot::Role::Pluggable {
12    use whatbot::Message;
13    use Class::Inspector;
14    use Class::Load qw(load_class);
15
16    has 'command'            => ( is => 'rw', isa => 'HashRef' );
17    has 'command_name'       => ( is => 'rw', isa => 'HashRef' );
18    has 'command_short_name' => ( is => 'rw', isa => 'HashRef' );
19    has 'skip_extensions'    => ( is => 'rw', isa => 'Int' );
20    has 'search_base'        => ( is => 'ro', default => 'whatbot::Command' );
21
22    method BUILD ($) {
23        $self->build_command_map();
24    }
25
26    method build_command_map {
27        my %command;        # Ordered list of commands
28        my %command_name;   # Maps command names to commands
29        my %command_short_name;
30       
31            # Scan whatbot::Command for loadable plugins
32        foreach my $class_name ( $self->plugins ) {
33            my @class_split = split( /\:\:/, $class_name );
34            my $name = pop(@class_split);
35
36            # Go away unless it's a root module
37            next unless ( pop(@class_split) eq 'Command' );
38
39            eval {
40                load_class($class_name);
41            };
42                if ($@) {
43                        $self->log->error( $class_name . ' failed to load: ' . $@ );
44                } else {
45                        unless ( $class_name->can('register') ) {
46                                $self->log->error( $class_name . ' failed to load due to missing methods' );
47                        } else {
48                            my @run_paths;
49                            my %end_paths;
50                            my $command_root = $class_name;
51                            $command_root =~ s/whatbot\:\:Command\:\://;
52                            $command_root = lc($command_root);
53                           
54                                # Instantiate
55                                my $config;
56                                if (defined $self->config->commands->{lc($name)}) {
57                                        $config = $self->config->commands->{lc($name)};
58                                }
59                                my $new_command = $class_name->new(
60                                        'base_component' => $self->parent->base_component,
61                                        'my_config'      => $config,
62                        'name'           => $command_root,
63                                );
64                                $new_command->controller($self);
65                               
66                                # Determine runpaths
67                                foreach my $function ( @{Class::Inspector->functions($class_name)} ) {
68                                    my $full_function = $class_name . '::' . $function;
69                                    my $coderef = \&$full_function;
70                                   
71                                    # Get subroutine attributes
72                                    if ( my $attributes = $new_command->FETCH_CODE_ATTRIBUTES($coderef) ) {
73                                        foreach my $attribute ( @{$attributes} ) {
74                                            my ( $command, $arguments ) = split( /\s*\(/, $attribute, 2 );
75                                           
76                                            if ( $command eq 'Command' ) {
77                                                my $register = '^' . $command_root . ' +' . $function . ' *([^\b]+)*';
78                                                if ( $command_name{$register} ) {
79                                                    $self->error_override( $class_name, $register )
80                                            } else {
81                                                        push(
82                                                            @run_paths,
83                                                            {
84                                                                'match'     => $register,
85                                                                'function'  => $function
86                                                            }
87                                                        );
88                                            }
89                                               
90
91                                            } elsif ( $command eq 'CommandRegEx' ) {
92                                                $arguments =~ s/\)$//;
93                                                unless ( $arguments =~ /^'.*?'$/ ) {
94                                                    $self->error_regex( $class_name, $function, $arguments );
95                                                } else {
96                                                        $arguments =~ s/^'(.*?)'$/$1/;
97                                                        my $register = '^' . $command_root . ' +' . $arguments;
98                                                        if ( $command_name{$register} ) {
99                                                            $self->error_override( $class_name, $register )
100                                                    } else {
101                                                        push(
102                                                            @run_paths,
103                                                            {
104                                                                'match'     => $register,
105                                                                'function'  => $function
106                                                            }
107                                                        );
108                                                    }
109                                        }
110                                                   
111                                            } elsif ( $command eq 'GlobalRegEx' ) {
112                                                $arguments =~ s/\)$//;
113                                                unless ( $arguments =~ /^'.*?'$/ ) {
114                                                    $self->error_regex( $class_name, $function, $arguments );
115                                                } else {
116                                                    $arguments =~ s/^'(.*?)'$/$1/;
117                                                        if ( $command_name{$arguments} ) {
118                                                            $self->error_override( $class_name, $arguments )
119                                                    } else {
120                                                        push(
121                                                            @run_paths,
122                                                            {
123                                                                'match'     => $arguments,
124                                                                'function'  => $function
125                                                            }
126                                                        );
127                                                    }
128                                                }
129                                               
130                                            } elsif ( $command eq 'Monitor' ) {
131                                                push(
132                                                    @run_paths,
133                                                    {
134                                                        'match'     => '',
135                                                        'function'  => $function
136                                                    }
137                                                );
138                                               
139                                            } elsif ( $command eq 'Event' ) {
140                                                $arguments =~ s/\)$//;
141                                                $arguments =~ s/^'(.*?)'$/$1/;
142                                                push(
143                                                    @run_paths,
144                                                    {
145                                                        'event'     => $arguments,
146                                                        'function'  => $function
147                                                    }
148                                                );
149                                               
150                                            } elsif ( $command eq 'StopAfter' ) {
151                                                $end_paths{$function} = 1;
152                                               
153                                            } else {
154                                                $self->log->error(
155                                                    $class_name . ': Invalid attribute "' . $command . '" on method "' . $function . '", ignoring.'
156                                                );
157                                            }
158                                        }
159                                    }
160                                }
161                               
162                                $new_command->command_priority('Extension') unless ( $new_command->command_priority );
163                                unless ( 
164                                    lc($new_command->command_priority) =~ /(extension|last)/
165                                    and $self->skip_extensions
166                                ) {
167                                        # Add to command structure and name to command map
168                                        $command{ lc($new_command->command_priority) }->{$class_name} = \@run_paths;
169                                        $command_name{$class_name} = $new_command;
170                                        $command_short_name{$command_root} = $new_command;
171                               
172                                        $self->log->write( '-> ' . ref($new_command) . ' loaded.' );
173                                }
174                               
175                                # Insert end paths
176                                for ( my $i = 0; $i < scalar(@run_paths); $i++ ) {
177                                    if ( $end_paths{ $run_paths[$i]->{'function'} } ) {
178                                        $run_paths[$i]->{'stop'} = 1;
179                                    }
180                                }
181                        }
182                }
183        }
184       
185        $self->command(\%command);
186        $self->command_name(\%command_name);
187        $self->command_short_name(\%command_short_name);
188    }
189
190    method handle_message ( $message, $me? ) {
191        my @messages;
192        foreach my $priority ( qw( primary core extension last ) ) {
193            last if ( @messages and $priority =~ /(extension|last)/ );
194           
195                # Iterate through priorities, in order, check for commands that can
196                # receive content
197                foreach my $command_name ( keys %{ $self->command->{$priority} } ) {
198                    my $command = $self->command_name->{$command_name};
199                    next if ( $command->require_direct and !$message->is_direct );
200
201                # Check each method corresponding to a registered runpath to see
202                # if it cares about our content
203                        foreach my $run_path ( @{ $self->command->{$priority}->{$command_name} } ) {
204                                next unless ( $run_path->{'match'} or $run_path->{'function'} );
205
206                            my $listen = ( $run_path->{'match'} or '' );
207                            my $function = $run_path->{'function'};
208                   
209                                if ( $listen eq '' or my (@matches) = $message->content =~ /$listen/i ) {
210                                        my $result = eval {
211                                                $command->$function( $message, \@matches );
212                                        };
213                        my $error = $@;
214                        return $self->_return_error( $command_name, $message, $error ) if ($error);
215                        $self->_parse_result( $command_name, $message, $result, \@messages );
216                               
217                                        # End processing for this command if StopAfter was called.
218                                        last if ( $run_path->{'stop'} );
219                               
220                                }
221                        }
222                }
223        }
224       
225        return \@messages;
226    }
227
228        # dear god refactor
229    method handle_event ( $target, $event, $user, $me? ) {
230        my ( $io, $context ) = split( /:/, $target );
231        my @messages;
232        foreach my $priority ( qw( primary core extension last ) ) {
233            last if ( @messages and $priority =~ /(extension|last)/ );
234           
235                # Iterate through priorities, in order, check for commands that can
236                # receive content
237                foreach my $command_name ( keys %{ $self->command->{$priority} } ) {
238                    my $command = $self->command_name->{$command_name};
239
240                # Check each method corresponding to a registered runpath to see
241                # if it cares about our content
242                        foreach my $run_path ( @{ $self->command->{$priority}->{$command_name} } ) {
243                                next unless ( $run_path->{'event'} and $run_path->{'event'} eq $event );
244
245                            my $function = $run_path->{'function'};
246                    my $message = whatbot::Message->new({
247                        'from'    => $me,
248                        'to'      => $context,
249                        'content' => '',
250                        'me'      => $me,
251                    });
252                                my $result = eval {
253                                        $command->$function( $target, $user );
254                                };
255                    my $error = $@;
256                    return $self->_return_error( $command_name, $message, $error ) if ($error);
257                    $self->_parse_result( $command_name, $message, $result, \@messages );
258                               
259                                # End processing for this command if StopAfter was called.
260                                last if $run_path->{'stop'};
261                       
262                        }
263                }
264        }
265       
266        return \@messages;
267    }
268
269    method _return_error( $command_name, $message, $error ) {
270        $self->log->error( 'Failure in ' . $command_name . ': ' . $error );
271        return $message->reply({
272            'content' => $command_name . ' completely failed at that last remark.',
273        });
274    }
275
276    # Parse the result from a event or message call
277    method _parse_result( $command_name, $message?, $result?, ArrayRef $messages? ) {
278        $message ||= whatbot::Message->new({
279            'from'    => '',
280            'to'      => 'public',
281            'content' => '',
282        });
283        if ( defined $result ) {
284            last if ( $result eq 'last_run' );
285   
286            $self->log->write( '%%% Message handled by ' . $command_name )
287                unless ( defined $self->config->io->[0]->{'silent'} );
288            $result = [ $result ] if ( ref($result) ne 'ARRAY' );
289       
290            foreach my $result_single ( @$result ) {
291                my $outmessage;
292                if ( ref($result_single) eq 'whatbot::Message' ) {
293                    $outmessage = $result_single;
294                    my $content = $outmessage->content;
295                    $content =~ s/!who/$message->from/;
296                    $outmessage->content($content);
297                } else {
298                    $result_single =~ s/!who/$message->from/;
299                    $outmessage = $message->reply({
300                        'content' => $result_single,
301                    });
302                }
303                push( @$messages, $outmessage );
304            }
305        }
306    }
307
308    method dump_command_map {
309        foreach my $priority ( qw( primary core extension ) ) {
310            my $commands = 0;
311           
312            $self->log->write( uc($priority) . ':' );
313           
314                foreach my $command_name ( keys %{ $self->command->{$priority} } ) {
315                        foreach my $run_path ( @{ $self->command->{$priority}->{$command_name} } ) {
316                                if ( $run_path->{'match'} ) {
317                                        $self->log->write( ' /' . $run_path->{'match'} . '/ => ' . $command_name . '->' . $run_path->{'function'} );
318                                } elsif ( $run_path->{'event'} ) {
319                                        $self->log->write( ' Event "' . $run_path->{'event'} . '" => ' . $command_name . '->' . $run_path->{'function'} );
320                                }
321                       
322                        $commands++;
323                }
324            }
325           
326            $self->log->write(' none') unless ($commands);
327        }
328    }
329
330    method error_override ( Str $class, Str $name ) {
331        $self->log->error( $class . ': More than one command being registered for "' . $name . '".' )
332    }
333
334    method error_regex ( Str $class, Str $function, Str $regex ) {
335        $self->log->error( 
336            $class . ': Invalid arguments (' . $regex . ') in method "' . $function . '".'
337        );
338    }
339}
340
3411;
342
343=pod
344
345=head1 NAME
346
347whatbot::Controller - Command processor and dispatcher
348
349=head1 SYNOPSIS
350
351 use whatbot::Controller;
352 
353 my $controller = whatbot::Controller->new();
354 $controller->build_command_map();
355 
356 ...
357 
358 my $messages = $controller->handle_message( $incoming_message );
359
360=head1 DESCRIPTION
361
362whatbot::Controller is the master command dispatcher for whatbot. When whatbot
363is started, Controller builds the run paths based on the attributes in the
364whatbot::Command namespace. When a message event is fired during runtime,
365Controller parses the message and directs the event to each appropriate
366command.
367
368=head1 METHODS
369
370=over 4
371
372=item handle_message( whatbot::Message $message )
373
374Run incoming message through commands, parse responses, and deliver back to IO.
375
376=item handle_event( $event, $user )
377
378Run incoming event through commands, parse responses, and delivery back to IO.
379
380=head1 INHERITANCE
381
382=over 4
383
384=item whatbot::Component
385
386=over 4
387
388=item whatbot::Controller
389
390=back
391
392=back
393
394=head1 LICENSE/COPYRIGHT
395
396Be excellent to each other and party on, dudes.
397
398=cut
Note: See TracBrowser for help on using the browser.