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.161950';
3 4     4   1087512 use v5.10;
  4         11  
4 4     4   15 use Moose();
  4         4  
  4         42  
5 4     4   1073 use Bot::Backbone::DispatchSugar();
  4         8  
  4         91  
6 4     4   28 use Moose::Exporter;
  4         4  
  4         17  
7 4     4   135 use Class::Load;
  4         5  
  4         155  
8              
9 4     4   1913 use Bot::Backbone::Meta::Class::Bot;
  4         3577  
  4         170  
10 4     4   1334 use Bot::Backbone::Dispatcher;
  4         815  
  4         1450  
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 419 shift;
23 4         21 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   23 my ($meta, $section, $class_name) = @_;
32              
33 12 100       75 if ($class_name =~ s/^\.//) {
    50          
34 3         13 $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         35 $class_name = join '::', 'Bot::Backbone', $section, $class_name;
41             }
42              
43 12         45 Class::Load::load_class($class_name);
44 12         174076 return $class_name;
45             }
46              
47             sub send_policy($%) {
48 3     3 1 106 my ($meta, $name, @config) = @_;
49              
50 3         5 my @final_config;
51 3         14 while (my ($class_name, $policy_config) = splice @config, 0, 2) {
52 3         12 $class_name = _resolve_class_name($meta, 'SendPolicy', $class_name);
53 3         25 push @final_config, [ $class_name, $policy_config ];
54             }
55              
56 3         161 $meta->add_send_policy($name, \@final_config);
57             }
58              
59              
60             sub service($%) {
61 9     9 1 7375 my ($meta, $name, %config) = @_;
62              
63 9         31 my $class_name = _resolve_class_name($meta, 'Service', $config{service});
64 9         24 $config{service} = $class_name;
65              
66 9         350 $meta->add_service($name, \%config);
67              
68 9 50       32 if (my $service_meta = Moose::Util::find_meta($class_name)) {
69 9 50 33     468 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 27 my ($meta, $name, $code) = @_;
78              
79 2         81 my $dispatcher = Bot::Backbone::Dispatcher->new;
80             {
81 2         5 $meta->building_dispatcher($dispatcher);
  2         77  
82 2         6 $code->();
83 2         73 $meta->no_longer_building_dispatcher;
84             }
85              
86 2         89 $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.161950
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             parameter 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