File Coverage

blib/lib/Bot/Backbone.pm
Criterion Covered Total %
statement 47 47 100.0
branch 5 8 62.5
condition 1 3 33.3
subroutine 12 12 100.0
pod 4 4 100.0
total 69 74 93.2


line stmt bran cond sub pod time code
1             package Bot::Backbone;
2             $Bot::Backbone::VERSION = '0.160630';
3 4     4   1008060 use v5.10;
  4         12  
4 4     4   14 use Moose();
  4         5  
  4         37  
5 4     4   972 use Bot::Backbone::DispatchSugar();
  4         7  
  4         89  
6 4     4   22 use Moose::Exporter;
  4         5  
  4         16  
7 4     4   137 use Class::Load;
  4         5  
  4         156  
8              
9 4     4   1877 use Bot::Backbone::Meta::Class::Bot;
  4         4109  
  4         182  
10 4     4   1766 use Bot::Backbone::Dispatcher;
  4         819  
  4         1964  
11              
12             # ABSTRACT: Extensible framework for building bots
13              
14              
15             Moose::Exporter->setup_import_methods(
16                 with_meta => [ qw( dispatcher service send_policy ) ],
17                 also => [ qw( Moose Bot::Backbone::DispatchSugar ) ],
18             );
19              
20              
21             sub init_meta {
22 4     4 1 329     shift;
23 4         16     return Moose->init_meta(@_,
24                     base_class => 'Bot::Backbone::Bot',
25                     metaclass => 'Bot::Backbone::Meta::Class::Bot',
26                 );
27             }
28              
29              
30             sub _resolve_class_name {
31 12     12   17     my ($meta, $section, $class_name) = @_;
32              
33 12 100       51     if ($class_name =~ s/^\.//) {
    50          
34 3         12         $class_name = join '::', $meta->name, $section, $class_name;
35                 }
36                 elsif ($class_name =~ s/^=//) {
37             # do nothing, we now have the exact name
38                 }
39                 else {
40 9         25         $class_name = join '::', 'Bot::Backbone', $section, $class_name;
41                 }
42              
43 12         35     Class::Load::load_class($class_name);
44 12         152288     return $class_name;
45             }
46              
47             sub send_policy($%) {
48 3     3 1 53     my ($meta, $name, @config) = @_;
49              
50 3         3     my @final_config;
51 3         10     while (my ($class_name, $policy_config) = splice @config, 0, 2) {
52 3         8         $class_name = _resolve_class_name($meta, 'SendPolicy', $class_name);
53 3         21         push @final_config, [ $class_name, $policy_config ];
54                 }
55              
56 3         113     $meta->add_send_policy($name, \@final_config);
57             }
58              
59              
60             sub service($%) {
61 9     9 1 6285     my ($meta, $name, %config) = @_;
62              
63 9         28     my $class_name = _resolve_class_name($meta, 'Service', $config{service});
64 9         19     $config{service} = $class_name;
65              
66 9         316     $meta->add_service($name, \%config);
67              
68 9 50       23     if (my $service_meta = Moose::Util::find_meta($class_name)) {
69 9 50 33     413         Moose::Util::ensure_all_roles($meta, $service_meta->all_bot_roles)
70                         if $service_meta->isa('Bot::Backbone::Meta::Class::Service')
71                        and $service_meta->has_bot_roles;
72                 }
73             }
74              
75              
76             sub dispatcher($$) {
77 2     2 1 20     my ($meta, $name, $code) = @_;
78              
79 2         65     my $dispatcher = Bot::Backbone::Dispatcher->new;
80                 {
81 2         2         $meta->building_dispatcher($dispatcher);
  2         62  
82 2         5         $code->();
83 2         71         $meta->no_longer_building_dispatcher;
84                 }
85              
86 2         65     $meta->add_dispatcher($name, $dispatcher);
87             }
88              
89              
90             1;
91              
92             __END__
93            
94             =pod
95            
96             =encoding UTF-8
97            
98             =head1 NAME
99            
100             Bot::Backbone - Extensible framework for building bots
101            
102             =head1 VERSION
103            
104             version 0.160630
105            
106             =head1 SYNOPSIS
107            
108             package MyBot;
109             use v5.14; # because newer Perl is cooler than older Perl
110             use Bot::Backbone;
111            
112             use DateTime;
113             use AI::MegaHAL;
114             use WWW::Wikipedia;
115            
116             service chat_bot => (
117             service => 'JabberChat',
118             jid => 'mybot@example.com',
119             password => 'secret',
120             host => 'example.com',
121             );
122            
123             service group_foo => (
124             service => 'GroupChat',
125             group => 'foo',
126             chat => 'chat_bot',
127             dispatcher => 'group_chat', # defined below
128             );
129            
130             # This would invoke a service named MyBot::Service::Pastebin
131             service pastebin => (
132             service => '.Pastebin',
133             chats => [ 'group_foo' ],
134             host => 'localhost',
135             port => 5000,
136             );
137            
138             has megahal => (
139             is => 'ro',
140             isa => 'AI::MegaHAL',
141             default => sub { AI::MegaHAL->new },
142             );
143            
144             has wikipedia => (
145             is => 'ro',
146             isa => 'WWW::Wikipedia',
147             default => sub { WWW::Wikipedia->new },
148             );
149            
150             dispatcher group_chat => as {
151             # Report the bot's time
152             command '!time' => respond { DateTime->now->format_cldr('ddd, MMM d, yyyy @ hh:mm:ss') };
153            
154             # Basic echo command, with arguments
155             command '!echo' => given_parameters {
156             argument echo_this => ( matching => qr/.*/ );
157             } respond {
158             my ($self, $message) = @_;
159             $message->arguments->{echo_this};
160             };
161            
162             # Include the pastebin commands (whatever they may be)
163             redispatch_to 'pastebin';
164            
165             # Look for wikiwords in a comment and report the summaries for each
166             also not_to_me respond {
167             my ($self, $message) = @_;
168            
169             my (@wikiwords) = $message->text =~ /\[\[(\w+)\]\]/g;
170            
171             map { "$_->[0]: " . $_->[1] }
172             grep { defined $_->[1] }
173             map { [ $_, $self->wikipedia->search($_) }
174             @wikiwords;
175             };
176            
177             # Return an AI::MegaHAL resopnse for any message address to the bot
178             to_me respond {
179             my ($self, $message) = @_;
180             $self->megahal->do_response($message->text);
181             };
182            
183             # Finally:
184             # - also: match even if something else already responded
185             # - not_command: but not if a command matched
186             # - not_to_me: but not if addressed to me
187             # - run: run this code, but do not respond
188             also not_command not_to_me run_this {
189             my ($self, $message) = @_;
190             $self->megahal->learn($message->text);
191             };
192             };
193            
194             my $bot = MyBot->new;
195             $bot->run;
196            
197             =head1 DESCRIPTION
198            
199             Bots should be easy to build. Also a bot framework does not need to be tied to a
200             particular protocol (e.g., IRC, Jabber, Slack, etc.). However, most bot tools
201             fail at either of these. Finally, it should be possible to create generic
202             services that a bot can consume or share with other bots. This framework aims at
203             solving all of these.
204            
205             This framework provides the following tools to this end.
206            
207             =head2 Services
208            
209             A service is a generic sub-application that runs within your bot, possibly
210             independent of the rest. Here are some examples of possible services:
211            
212             =over
213            
214             =item Chat Service
215            
216             Each chat server connects to a chat service. This might be a Jabber server or an
217             IRC server or Slack or even just a local REPL for running commands on the
218             console. A single bot may have multiple connections to these servers by running
219             more than one chat service.
220            
221             See L<Bot::Backbone::Service::JabberChat> and
222             L<Bot::Backbone::Service::ConsoleChat> for examples.
223            
224             See L<Bot::Backbone::Service::Role::Chat> for responsibilities.
225            
226             =item Group Service
227            
228             These will ask a chat service to join a particular room or channel.
229            
230             See L<Bot::Backbone::Service::GroupChat>.
231            
232             =item Direct Message Service
233            
234             These services are similar to Channel services, but are used to connect to
235             another individual account or a list of other accounts.
236            
237             See L<Bot::Backbone::Service::DirectChat>.
238            
239             =item Dispatched Service
240            
241             A dispatched service may provide a group of common commands to the dispatcher.
242            
243             See L<Bot::Backbone::Service> for help on building such a service and see
244             L<Bot::Backbone::Service::Role::ChatConsumer> for responsibilities.
245            
246             =item Other Services
247            
248             These could do anything you could imagine: search the web or your wiki, check
249             your email, notify you of new messages, monitor server logs, run RiveScript, run
250             a Markov Chain-based conversation, manage a pastebin, play Russian Roulette, or
251             whatever.
252            
253             I have written a few of these services and may publish someday in separate
254             projects in the future.
255            
256             =back
257            
258             Basically, services are the place for any kind of tool the bot might need.
259             Simple services might be embedded into the bot itself, but it's recommended for
260             simplicity that the large dispatcher above not be emulated. Instead separate
261             each sub-application in your bot into a service to make them easier to maintain
262             separately.
263            
264             =head2 Dispatcher
265            
266             A dispatcher is a collection of predicates paired with run modes. A dispatcher
267             may be applied to a chat, channel, or direct message service to handle incoming
268             messages. When a message comes in from the service, each predicate is checked
269             against that message. The run mode of the first matching predicate is executed
270             (as well as any C<also> predicates.
271            
272             Dispatchers are extensible, allowing for new predicates and run mode operations
273             to be defined as needed.
274            
275             =head1 SUBROUTINES
276            
277             =head2 init_meta
278            
279             Setup the bot package with L<Bot::Backbone::Meta::Class> as the meta class and L<Bot::Backbone::Bot> as the base class.
280            
281             =head1 SETUP ROUTINES
282            
283             =head2 send_policy
284            
285             send_policy $name => ( ... );
286            
287             Add a new send policy configuration.
288            
289             =head2 service
290            
291             service $name => ( ... );
292            
293             Add a new service configuration.
294            
295             =head2 dispatcher
296            
297             dispatcher $name => ...;
298            
299             This predicate is provided at the top level and is usually paired with the
300             L</as> run mode operation, though it could be paired with any of them. This
301             declares a named dispatcher that can be referred to as the C<dispatcher>
302             attribute on services that support dispatching.
303            
304             =head1 DISPATCHER PREDICATES
305            
306             =head2 redispatch_to
307            
308             redispatch_to 'service_name';
309            
310             Given a service name for a service implementing L<Bot::Backbone::Service::Role::Dispatch>, we will ask the dispatcher on that object (if any) to perform dispatch.
311            
312             =head2 command
313            
314             command $name => ...;
315            
316             A command predicate matches the very first word found in the incoming message
317             text. It only matches an exact string and only messages not preceded by
318             whitespace (unless the message is addressed to the bot, in which case whitespace
319             is allowed).
320            
321             =head2 not_command
322            
323             not_command ...;
324            
325             This is not useful unless paired with the L</also> predicate. This only matches
326             if no command has been matched so far for the current message.
327            
328             =head2 given_parameters
329            
330             given_parameters { parameter $name => %config; ... } ...
331            
332             This is used in conjunction with C<parameter> to define arguments expected to
333             come next.
334            
335             If the C<given_parameters> predicate matches completely, the message will have
336             each of the named parameters set on the C<parameters> hash inside the nested
337             L</run_this> or L</respond>.
338            
339             The C<%config> may contain the following keys:
340            
341             =over
342            
343             =item match
344            
345             This is a string a regular expression that will be used to match against the
346             next part of the input, as if it were a command-line. The string or expression
347             must match the entire next chunk (or provide a default) or the dispatcher will
348             move on to the next dispatch predicate.
349            
350             =item match_original
351            
352             Rather than matching the next command-line split chunk of the input, this
353             matches some next portion of the string. If it matches or there is a default
354             provided, success.
355            
356             =item default
357            
358             This sets the default. If this is set, the parameter match will always succeed
359             and gain this default value if the match itself fails.
360            
361             =back
362            
363             You must provide either C<match> or C<match_original> in each parameter.
364             Parameters my interleave C<match> and C<match_original> style matches as well
365             and Backbone should do the right thing.
366            
367             =head2 to_me
368            
369             to_me ...
370            
371             Matches messages that are considered directed toward the bot. This may be a
372             direct message or a channel message prefixed by the bot's name.
373            
374             =head2 not_to_me
375            
376             not_to_me ...
377            
378             This is the opposite of L</to_me>. It matches any message not sent directly to
379             the bot.
380            
381             =head2 shouted
382            
383             shouted ...
384            
385             Matches messages that are received from outside the current chat, such as a system message or administrator alert sent to all channels.
386            
387             head2 spoken
388            
389             spoken ...
390            
391             Matches messages that are stated within the channel to all participants. This is the usual volume level.
392            
393             =head2 whispered
394            
395             whispered ...
396            
397             Matches messages that are stated within the channel to only a subset of the listeners, such as a private message within a channel.
398            
399             =head2 also
400            
401             also ...;
402            
403             In general, only the run mode operation for the first matching predicate will be
404             executed. The C<also> predicate, however, tells the dispatcher to try and match
405             against it even if the dispatcher has already responded.
406            
407             =head1 RUN MODE OPERATIONS
408            
409             =head2 as
410            
411             as { ... }
412            
413             This nests another set of dispatchers inside a predicate. Each set of predicates
414             defined within will be executed in turn if this run mode oepration is reached.
415            
416             =head2 respond
417            
418             respond { ... }
419            
420             If a C<response> is executed, the code ref given will be executed with three
421             arguments. The first will be a reference to the bot's main object. The second
422             will be a message object describing the incoming message. The third is the
423             service that sent the message.
424            
425             The return value of the executed code ref will be used to respond to the user.
426             It will be called in list context and all the values returned will be sent to
427             the user. If an empty list or C<undef> is returned, then no message will be sent
428             to the user and dispatching will continue as if the predicate had not matched.
429            
430             =head2 respond_with_method
431            
432             respond_with_method 'method_name'
433            
434             Given the name of a method defined on the current bot package, that method will be called if all the dispatch predicates in front of it match.
435            
436             It is called and used exactly as described under L</respond>.
437            
438             =head2 respond_with_service_method
439            
440             respond_with_service_method 'method_name'
441            
442             This directive should only be used with dispatchers created in the bot class and then assigned to the service using the L<Bot::Backbone::Service::Role::Dispatch/dispatcher> attribute. This allows the bot to define dispatchers on behalf of a service, which still calls the services methods.
443            
444             =head2 run_this
445            
446             run_this { ... }
447            
448             This will execute the given code ref, passing it the reference to the bot, the
449             message, and the service as arguments. The return value is ignored.
450            
451             =head2 run_this_method
452            
453             run_this_method 'method_name'
454            
455             This will execute the named method on the bot class. It will be called and used in exactly the same way as L</run_this>.
456            
457             =head2 run_this_service_method
458            
459             run_this_service_method 'method_name'
460            
461             This dispatch directive should only be used on dispatchers that are assigned to a service using the L<Bot::Backbone::Service::Role::Dispatch/dispatcher> argument. It allows the bot to specify a custom dispatcher for that service that still calls that service's methods.
462            
463             =head1 AUTHOR
464            
465             Andrew Sterling Hanenkamp <hanenkamp@cpan.org>
466            
467             =head1 COPYRIGHT AND LICENSE
468            
469             This software is copyright (c) 2016 by Qubling Software LLC.
470            
471             This is free software; you can redistribute it and/or modify it under
472             the same terms as the Perl 5 programming language system itself.
473            
474             =cut
475