File Coverage

blib/lib/Bot/Backbone/SendPolicy/MinimumRepeatInterval.pm
Criterion Covered Total %
statement 28 37 75.6
branch 7 14 50.0
condition 0 3 0.0
subroutine 5 5 100.0
pod 2 2 100.0
total 42 61 68.8


line stmt bran cond sub pod time code
1             package Bot::Backbone::SendPolicy::MinimumRepeatInterval;
2             $Bot::Backbone::SendPolicy::MinimumRepeatInterval::VERSION = '0.160630';
3 1     1   651 use v5.10;
  1         3  
4 1     1   3 use Moose;
  1         1  
  1         6  
5              
6             with 'Bot::Backbone::SendPolicy';
7              
8 1     1   4081 use AnyEvent;
  1         2  
  1         353  
9              
10             # ABSTRACT: Prevent any message from being repeated too often
11              
12              
13             has interval => (
14                 is => 'ro',
15                 isa => 'Num',
16                 required => 1,
17             );
18              
19              
20             has queue_length => (
21                 is => 'ro',
22                 isa => 'Int',
23                 predicate => 'has_queue',
24             );
25              
26              
27             has discard => (
28                 is => 'ro',
29                 isa => 'Bool',
30                 required => 1,
31                 default => 0,
32             );
33              
34              
35             has lingering_interval => (
36                 is => 'ro',
37                 isa => 'Num',
38                 predicate => 'has_lingering_interval',
39             );
40              
41              
42             has cache_key => (
43                 is => 'ro',
44                 isa => 'CodeRef',
45                 required => 1,
46                 default => sub { sub { $_[0]->{text} } },
47                 traits => [ 'Code' ],
48                 handles => {
49                     'get_cache_key' => 'execute',
50                 },
51             );
52              
53              
54             has send_cache => (
55                 is => 'ro',
56                 isa => 'HashRef[ArrayRef[Num]]',
57                 required => 1,
58                 default => sub { +{} },
59                 traits => [ 'Hash' ],
60                 handles => {
61                     'list_cache_keys' => 'keys',
62                     'delete_cache_key' => 'delete',
63                     'last_send_times' => 'get',
64                     'set_last_send_times' => 'set',
65                     'has_cache_key' => 'defined',
66                 },
67             );
68              
69              
70             sub purge_send_cache {
71 299     299 1 275     my $self = shift;
72              
73 299         4674     my $now = AnyEvent->now;
74 299         11641     for my $key ($self->list_cache_keys) {
75 594         461         my ($last_send, $orig_send) = @{ $self->last_send_times($key) };
  594         21520  
76              
77             # Delete if it's been longer than interval since last send
78 594 100       17193         $self->delete_cache_key($key)
79                         if $last_send + $self->interval < $now;
80                 }
81             }
82              
83              
84             sub allow_send {
85 299     299 1 430     my ($self, $options) = @_;
86              
87 299         465     $self->purge_send_cache;
88              
89 299         669     my %send = ( allow => 1 );
90 299         4345     my $now = AnyEvent->now;
91 299         11212     my $key = $self->get_cache_key($options);
92 299         334     my $save = 1;
93 299         214     my $after = 0;
94 299         329     my ($last_send, $orig_send) = ($now, $now);
95              
96 299 100       10847     if ($self->has_cache_key($key)) {
97              
98             # If there's already a cache key in place, don't save
99 291         280         $save = 0;
100              
101             # Discard immediately if requested
102 291 50       8373         if ($self->discard) {
103 291         354             $send{allow} = 0;
104                     }
105              
106             # Otherwise, determine how long to delay sending
107                     else {
108 0         0             ($last_send, $orig_send) = @{ $self->last_send_times($key) };
  0         0  
109              
110             # Wait for whatever is left of the interval since the last send
111 0         0             $send{after} = $after = ($last_send + $self->interval) - $now;
112              
113             # If we have a lingering interval, we need to modify the send cache
114 0 0       0             if ($self->has_lingering_interval) {
115 0         0                 $save = 1;
116              
117             # The lingering interval has not been passed, so move the last
118             # send date forward
119 0 0       0                 if ($now - $orig_send < $self->lingering_interval) {
120 0         0                     $last_send = $now + $after;
121                             }
122              
123             # The lingering interval has passed, so move it back to the
124             # original, which should guarantee it is purged next cycle
125                             else {
126 0         0                     $last_send = $orig_send;
127                             }
128                         }
129              
130             # If the number of messages queued is too long, nevermind...
131 0 0 0     0             $send{allow} = 0
132                             if $self->has_queue
133                            and $after / $self->interval > $self->queue_length;
134                     }
135                 }
136              
137 299 100       823     $self->set_last_send_times($key, [ $last_send, $orig_send ]) if $save;
138 299         640     return \%send;
139             }
140              
141             __PACKAGE__->meta->make_immutable;
142              
143             __END__
144            
145             =pod
146            
147             =encoding UTF-8
148            
149             =head1 NAME
150            
151             Bot::Backbone::SendPolicy::MinimumRepeatInterval - Prevent any message from being repeated too often
152            
153             =head1 VERSION
154            
155             version 0.160630
156            
157             =head1 SYNOPSIS
158            
159             send_policy dont_repeat_yourself => (
160             MinimumRepeatInterval => {
161             interval => 5 * 60,
162             discard => 1,
163             linger_interval => 60 * 60,
164             },
165             );
166            
167             =head1 DESCRIPTION
168            
169             This send policy will prevent a particular message text from being sent more frequently than the permitted L</interval>.
170            
171             For example, suppose you have a service which does a Wikipedia lookup each time someone uses a WikiWord and states the link and first sentence from the article. It would be terribly annoying if, during a heated discussion of this article, when the WikiWord were repeated often, if that resulted in the bot posting and re-posting that sentence and link over and over again. With this policy in place, you don't have to worry about that happening.
172            
173             =head1 ATTRIBUTES
174            
175             =head2 interval
176            
177             This is the length of time in fractional seconds during which the bot is not permitted to repeat any particular message.
178            
179             =head2 queue_length
180            
181             This is the maximum number of messages that will be queued for later display before the messages will be discarded. If L</discard> is set to false, it is recommended that you set this value to something reasonable.
182            
183             =head2 discard
184            
185             When set to a true value, any messasge sent too soon will be discarded immediately. The default is false.
186            
187             =head2 lingering_interval
188            
189             The L</interval> determines how long the bot must wait before sending a duplicate message text. The lingering interval allows the normal interval to be extended with each new attempt to send the duplicate message text. The extension will occur according to the usual C<interval>, but will not be extended being the values set in fractional seconds on the C<lingering_interval>.
190            
191             For example, suppose you have interval set to 5 seconds and lingering interval set to 20 seconds. The bot tries to send the message "blah" and then tries again 3 seconds later and then again 6 seconds after the original. Both of these followup attempts will blocked. Assume this continues at 3 second intervals for 60 seconds. All the messages will be blocked except that first message, the message coming at 21 seconds and 42 seconds.
192            
193             =head2 cache_key
194            
195             The documentation in this module fudges a little in how this works. It's actually more flexible than it might seem. Normally, this send policy works based upon the actual message text sent by the user. However, in some cases this might not be convenient. In case you want to make the send policy depend on some other aspect of the message other than the message text, just replace the default C<cache_key> with a new subroutine.
196            
197             The given subroutine will be passed a single argument, the options hash reference sent to L</allow_send>. It must return a string (i.e., whatever is returned will be stringified). That string will be used as the cache key.
198            
199             This is an advanced feature. If you can't think of a reason why you'd want to use it, you probably don't want to. This is why the rest of the documentation will assumes the message text, but it's really caching according to whatever this little subroutine returns.
200            
201             =head2 send_cache
202            
203             This is the actual structure used to determine how recently a particular message text was last sent. Each time the send policy is called, it will be purged of any keys that are no longer relevant.
204            
205             It should be safe to save this structure using L<JSON> or L<YAML> or L<MongoDB> or L<Storable> or whatever you like and load it again, if you want the bot's C<send_cache> to survive restarts. However, the structure itself should be considered opaque and might change in a future release of L<Bot::Backbone>. It may even be removed altogether in a future release since there are lots of handy caching tools on the CPAN that might be used in place of this manual one.
206            
207             =head1 METHODS
208            
209             =head2 purge_send_cache
210            
211             $self->purge_send_cache;
212            
213             This method may go away in a future release depending on the fate of L</send_cache>. In the meantime, however, this method is used clear the C<send_cache> of expired cache keys.
214            
215             =head2 allow_send
216            
217             This applies the send policy to the message.
218            
219             =head1 AUTHOR
220            
221             Andrew Sterling Hanenkamp <hanenkamp@cpan.org>
222            
223             =head1 COPYRIGHT AND LICENSE
224            
225             This software is copyright (c) 2016 by Qubling Software LLC.
226            
227             This is free software; you can redistribute it and/or modify it under
228             the same terms as the Perl 5 programming language system itself.
229            
230             =cut
231