File Coverage

blib/lib/Net/Mollom.pm
Criterion Covered Total %
statement 94 133 70.6
branch 25 56 44.6
condition 24 48 50.0
subroutine 19 20 95.0
pod 8 8 100.0
total 170 265 64.1


line stmt bran cond sub pod time code
1             package Net::Mollom;
2 9     9   320785 use Any::Moose;
  9         519986  
  9         74  
3 9     9   18073 use XML::RPC;
  9         139902  
  9         331  
4 9     9   21814 use DateTime;
  9         2398379  
  9         583  
5 9     9   1606 use Params::Validate qw(validate SCALAR UNDEF);
  9         20  
  9         799  
6 9     9   9110 use Digest::HMAC_SHA1 qw(hmac_sha1);
  9         79423  
  9         609  
7 9     9   8262 use MIME::Base64 qw(encode_base64);
  9         7438  
  9         623  
8 9     9   68 use DateTime;
  9         18  
  9         279  
9 9     9   54 use Carp qw(carp croak);
  9         20  
  9         602  
10 9     9   5786 use Net::Mollom::ContentCheck;
  9         26  
  9         586  
11             use Exception::Class (
12 9         123 'Net::Mollom::Exception',
13             'Net::Mollom::ServerListException' => {isa => 'Net::Mollom::Exception'},
14             'Net::Mollom::CommunicationException' => {isa => 'Net::Mollom::Exception'},
15             'Net::Mollom::APIException' =>
16             {isa => 'Net::Mollom::Exception', fields => [qw(mollom_code mollom_desc)]},
17 9     9   16977 );
  9         123665  
18              
19             has current_server => (is => 'rw', isa => 'Num', default => 0);
20             has public_key => (is => 'rw', isa => 'Str', required => 1);
21             has private_key => (is => 'rw', isa => 'Str', required => 1);
22             has session_id => (is => 'rw', isa => 'Str');
23             has xml_rpc => (is => 'rw', isa => 'XML::RPC');
24             has warnings => (is => 'rw', isa => 'Bool', default => 1);
25             has attempt_limit => (is => 'rw', isa => 'Num', default => 1);
26             has attempts => (is => 'rw', isa => 'Num', default => 0);
27             has servers_init => (is => 'rw', isa => 'Bool', default => 0);
28             has servers => (
29             is => 'rw',
30             isa => 'ArrayRef',
31             default => sub {
32             ['http://xmlrpc1.mollom.com', 'http://xmlrpc2.mollom.com', 'http://xmlrpc3.mollom.com'];
33             },
34             );
35              
36 9     9   30773 no Any::Moose;
  9         26  
  9         89  
37             __PACKAGE__->meta->make_immutable;
38              
39             our $API_VERSION = '1.0';
40             our $VERSION = '0.09';
41             my $ERROR_PARSE = 1000;
42             my $ERROR_REFRESH_SERVERS = 1100;
43             my $ERROR_NEXT_SERVER = 1200;
44             my $MAX_API_TRIES = 10;
45              
46             =head1 NAME
47              
48             Net::Mollom - interface with Mollom web API
49              
50             =head1 SYNOPSIS
51              
52             Communicate with the Mollom web API (L) via
53             XML-RPC to determine whether user input is Spam, Ham, flame or
54             obscene.
55              
56             my $mollom = Net::Mollom->new(
57             public_key => 'a2476604ffba00c907478c8f40b83b03',
58             private_key => '42d5448f124966e27db079c8fa92de0f',
59             );
60              
61             my @server_list = $mollom->server_list();
62              
63             my $check = $mollom->check_content(
64             post_title => $title,
65             post_body => $text,
66             );
67             if ($check->is_spam) {
68             warn "someone's trying to sell us v1@grA!";
69             } elsif ($check->is_unsure) {
70              
71             # show them a CAPTCHA to see if they are really human
72             my $captcha_url = $mollom->get_image_captcha();
73             } elsif ($check->quality < .5) {
74             warn "someone's trying to flame us!";
75             }
76              
77             If you have any questions about how any of the methods work, please
78             consult the Mollom API documentation - L.
79              
80             =head1 CONSTRUCTORS
81              
82             =head2 new
83              
84             This creates a new NET::Mollom object for communication. It takes the following
85             named arguments:
86              
87             =over
88              
89             =item * public_key (required)
90              
91             This is your Mollom API public key.
92              
93             =item * private_key (required)
94              
95             This is your Mollom API private key.
96              
97             =item * attempt_limit
98              
99             This is the number of times Net::Mollom will try to refresh the server list
100             before giving up. Defaults to 1.
101              
102             =item * warnings
103              
104             This boolean turns on warnings. You will get warnings for the following
105             situations:
106              
107             =over
108              
109             =item * A Mollom server is busy and we need to try a different one.
110              
111             =item * We have exhausted the list of servers to try and we need to get a new list.
112              
113             =back
114              
115             =back
116              
117             =head1 OBJECT METHODS
118              
119             =head2 verify_key
120              
121             Check to make sure that Mollom recognizes your public and private keys.
122             Returns true if successful, false otherwise. This is not necessary to use
123             in your application, but can be used when doing initial development or testing.
124              
125             if( $mollom->verify_key ) {
126             # go a head and do stuff
127             } else {
128             # doh! you screwed up somewhere
129             }
130              
131             =cut
132              
133             sub verify_key {
134 3     3 1 1288 my $self = shift;
135             # get the server list from Mollom if we don't already have one
136 3 50       23 $self->server_list() unless $self->servers_init;
137 1         6 return $self->_make_api_call('verifyKey');
138             }
139              
140             =head2 check_content
141              
142             Check some content for spamminess and quality. Takes the following
143             optional named arguments:
144              
145             =over
146              
147             =item * post_title
148              
149             =item * post_body
150              
151             =item * author_name
152              
153             =item * author_url
154              
155             =item * author_mail
156              
157             =item * author_openid
158              
159             =item * author_ip
160              
161             =item * author_id
162              
163             =back
164              
165             Returns a L object.
166              
167             my $check = $mollom->check_content(
168             post_title => $title,
169             post_body => $body,
170             author_name => 'Michael Peters',
171             author_mail => 'mpeters@p3.com',
172             author_id => 12345,
173             );
174              
175             =cut
176              
177             sub check_content {
178 4     4 1 4902 my $self = shift;
179 4         452 my %args = validate(
180             @_,
181             {
182             post_title => {type => SCALAR | UNDEF, optional => 1},
183             post_body => {type => SCALAR | UNDEF, optional => 1},
184             author_name => {type => SCALAR | UNDEF, optional => 1},
185             author_url => {type => SCALAR | UNDEF, optional => 1},
186             author_mail => {type => SCALAR | UNDEF, optional => 1},
187             author_openid => {type => SCALAR | UNDEF, optional => 1},
188             author_ip => {type => SCALAR | UNDEF, optional => 1},
189             author_id => {type => SCALAR | UNDEF, optional => 1},
190             session_id => {type => SCALAR | UNDEF, optional => 1},
191             }
192             );
193              
194             # we need at least 1 arg
195 4         20 croak "You must pass at least 1 argument to check_content!"
196 3 100 66     213 unless %args && map { defined $args{$_} } keys %args;
197              
198             # get the server list from Mollom if we don't already have one
199 2 50       25 $self->server_list() unless $self->servers_init;
200 0         0 my $results = $self->_make_api_call('checkContent', \%args);
201              
202             # remember the session_id so we can pass it along in future calls
203 0         0 $self->session_id($results->{session_id});
204              
205 0 0       0 return Net::Mollom::ContentCheck->new(
    0          
    0          
206             is_ham => $results->{spam} == 1 ? 1 : 0,
207             is_spam => $results->{spam} == 2 ? 1 : 0,
208             is_unsure => $results->{spam} == 3 ? 1 : 0,
209             quality => $results->{quality},
210             session_id => $results->{session_id},
211             );
212             }
213              
214             =head2 session_id
215              
216             This is the Mollom assigned session id. If you've made a call to
217             C it will be set by Mollom and you must pass it later
218             to any calls you make to C, C,
219             C or C. If you use the same Mollom
220             object that made the C call then you don't need to do
221             anything since it will remember that for you. But in most web applications
222             the next request by a user will not be served by the next process or
223             even the next server, so there's no guarantee. You need to store and
224             remember this mollom session_id on your own.
225              
226             =head2 send_feedback
227              
228             Send feedback to Mollom about their rating of your content. Take sthe following
229             optional named parameters:
230              
231             =over
232              
233             =item * feedback
234              
235             A string value of either C, C, C, or C.
236              
237             =item * session_id
238              
239             The id of the session where the content was checed (by a call to C).
240              
241             =back
242              
243             $mollom->send_feedback
244              
245             =cut
246              
247             sub send_feedback {
248 0     0 1 0 my $self = shift;
249 0         0 my %args = validate(
250             @_,
251             {
252             feedback => { type => SCALAR, regex => qr/^(spam|profanity|low-quality|unwanted)$/ },
253             session_id => { type => SCALAR | UNDEF, optional => 1 },
254             }
255             );
256 0   0     0 $args{session_id} ||= $self->session_id;
257              
258             # get the server list from Mollom if we don't already have one
259 0 0       0 $self->server_list() unless $self->servers_init;
260 0         0 return $self->_make_api_call('sendFeedback', \%args);
261             }
262              
263             =head2 get_image_captcha
264              
265             Returns the URL of an image CAPTCHA. This should only be called if the last
266             message checked was marked C. Not for C or C.
267             It takes the following optional parameters:
268              
269             =over
270              
271             =item * author_ip
272              
273             The IP address of the content author
274              
275             =item * session_id
276              
277             The Mollom session_id. Normally you don't need to worry about this since Net::Mollom
278             will take care of it for you.
279              
280             =back
281              
282             =cut
283              
284             sub get_image_captcha {
285 3     3 1 3168 my $self = shift;
286 3         329 my %args = validate(
287             @_,
288             {
289             author_ip => { type => SCALAR | UNDEF, optional => 1 },
290             session_id => { type => SCALAR | UNDEF, optional => 1 },
291             }
292             );
293 2   33     29 $args{session_id} ||= $self->session_id;
294              
295             # get the server list from Mollom if we don't already have one
296 2 50       15 $self->server_list() unless $self->servers_init;
297 0         0 my $results = $self->_make_api_call('getImageCaptcha', \%args);
298 0         0 $self->session_id($results->{session_id});
299 0         0 return $results->{url};
300             }
301              
302             =head2 get_audio_captcha
303              
304             Returns the URL of an audio CAPTCHA (mp3 file). This should only be called if the last
305             message checked was marked C. Not for C or C.
306             It takes the following optional parameters:
307              
308             =over
309              
310             =item * author_ip
311              
312             The IP address of the content author
313              
314             =item * session_id
315              
316             The Mollom session_id. Normally you don't need to worry about this since Net::Mollom
317             will take care of it for you.
318              
319             =back
320              
321             =cut
322              
323             sub get_audio_captcha {
324 2     2 1 2145 my $self = shift;
325 2         325 my %args = validate(
326             @_,
327             {
328             author_ip => { type => SCALAR | UNDEF, optional => 1 },
329             session_id => { type => SCALAR | UNDEF, optional => 1 },
330             }
331             );
332 1   33     19 $args{session_id} ||= $self->session_id;
333              
334             # get the server list from Mollom if we don't already have one
335 1 50       13 $self->server_list() unless $self->servers_init;
336 0         0 my $results = $self->_make_api_call('getAudioCaptcha', \%args);
337 0         0 $self->session_id($results->{session_id});
338 0         0 return $results->{url};
339             }
340              
341             =head2 check_captcha
342              
343             Check that what the user entered matches the last CAPTCHA that Mollom
344             sent as part of this session. Takes the following named arguments:
345              
346             =over
347              
348             =item * solution
349              
350             The user's answer to the CAPTCHA
351              
352             =item * session_id
353              
354             The id of the Mollom session.
355              
356             =back
357              
358             Returns true if correct, false otherwise.
359              
360             =cut
361              
362             sub check_captcha {
363 1     1 1 909 my $self = shift;
364 1         271 my %args = validate(
365             @_,
366             {
367             solution => { type => SCALAR },
368             session_id => { type => SCALAR | UNDEF, optional => 1 },
369             }
370             );
371 0   0     0 $args{session_id} ||= $self->session_id;
372              
373             # get the server list from Mollom if we don't already have one
374 0 0       0 $self->server_list() unless $self->servers_init;
375 0         0 return $self->_make_api_call('checkCaptcha', \%args);
376             }
377              
378             =head2 server_list
379              
380             This method will ask Mollom what servers to use. The list of servers
381             is saved in the Net::Mollom package and reused on subsequent calls
382             to the API. Normally you won't need to call this method on it's own
383             since it will be called for you when you use another part of the API.
384              
385             my @servers = $mollom->server_list();
386              
387             # or if you've saved the list in a more permanent data store
388             $mollom->server_list(@servers);
389              
390             =cut
391              
392             sub server_list {
393 12     12 1 900 my ($self, @list) = @_;
394 12 50       110 if( @list ) {
    50          
395 0         0 $self->servers(\@list);
396 0         0 $self->current_server(0);
397             } elsif(!$self->servers_init) {
398             # get our list from their API
399 12         83 my $results = $self->_make_api_call('getServerList');
400 3         17 $self->servers($results);
401 3         19 $self->servers_init(1);
402 3         14 $self->current_server(0);
403             }
404 3         6 return @{$self->servers};
  3         25  
405             }
406              
407             =head2 get_statistics
408              
409             This method gets your Mollom usage statistics. It takes the following required named
410             parameters:
411              
412             =over
413              
414             =item * type
415              
416             Must be one of C, C, C, C,
417             C, C, C.
418              
419             =back
420              
421             Will return the count for the specific statistic type you requested.
422              
423             =cut
424              
425             sub get_statistics {
426 3     3 1 2261 my $self = shift;
427 3         315 my %args = validate(
428             @_,
429             {
430             type => {
431             type => SCALAR,
432             regex =>
433             qr/^(total_(days|accepted|rejected)|yesterday_(accepted_rejected)|today_(accepted_rejected))$/
434             },
435             }
436             );
437              
438             # get the server list from Mollom if we don't already have one
439 1 50       25 $self->server_list() unless $self->servers_init;
440 0         0 return $self->_make_api_call('getStatistics', \%args);
441             }
442              
443             sub _make_api_call {
444 15     15   41 my ($self, $function, $args) = @_;
445 15         70 my $secret = $self->private_key;
446 15         34 my @servers = @{$self->servers};
  15         92  
447              
448             # keep track of how many times we've descended down into this rabbit hole
449 15 100       112 if( ! $self->{_recurse_level} ) {
450 10         31 $self->{_recurse_level} = 1;
451             } else {
452 5         10 $self->{_recurse_level}++;
453             }
454              
455 15 100       92 if (!$self->xml_rpc) {
456 10         22 my $xml_rpc = eval { XML::RPC->new($servers[$self->current_server] . '/' . $API_VERSION) };
  10         170  
457 10 50       310 Net::Mollom::CommunicationException->throw(error => $@) if $@;
458 10         114 $self->xml_rpc($xml_rpc);
459             }
460              
461 15   66     147 $args->{public_key} ||= $self->public_key;
462 15   66     218 $args->{time} ||= DateTime->now->strftime('%Y-%m-%dT%H:%M:%S.000%z');
463 15   66     8898 $args->{nonce} ||= int(rand(2_147_483_647)); # rand 32 bit integer
464 15   66     215 $args->{hash} ||=
465             encode_base64(hmac_sha1(join(':', $args->{time}, $args->{nonce}, $secret), $secret));
466              
467 15 0 66     855 if ( $function ne 'getServerList'
      33        
      33        
468             && $function ne 'verifyKey'
469             && $function ne 'getStatistics'
470             && $self->session_id)
471             {
472 0         0 $args->{session_id} = $self->session_id;
473             }
474              
475 15         31 my $results = eval { $self->xml_rpc->call("mollom.$function", $args) };
  15         141  
476 15 100       2725213 Net::Mollom::CommunicationException->throw(error => $@) if $@;
477              
478             # check if there are any errors and handle them accordingly
479 8 100 100     203 if (ref $results && (ref $results eq 'HASH') && $results->{faultCode}) {
      66        
480 4         16 my $fault_code = $results->{faultCode};
481 4 100 66     50 if (($fault_code == $ERROR_REFRESH_SERVERS) && ($self->{_recurse_level} < $MAX_API_TRIES) ) {
    50 33        
    50          
482 2 50       11 if ($function eq 'getServerList') {
483 0         0 delete $self->{_recurse_level};
484 0         0 Net::Mollom::ServerListException->throw(error => "Could not get list of servers from Mollom!");
485             } else {
486 2         22 $self->servers_init(0);
487 2         11 $self->server_list;
488 2         11 return $self->_make_api_call($function, $args);
489             }
490             } elsif (($fault_code == $ERROR_NEXT_SERVER) && ($self->{_recurse_level} < $MAX_API_TRIES)) {
491 0 0       0 carp("Mollom server busy, trying the next one.") if $self->warnings;
492 0         0 my $next_index = $self->current_server + 1;
493 0 0       0 if ($servers[$next_index] ) {
494 0         0 $self->current_server($next_index);
495 0         0 return $self->_make_api_call($function, $args);
496             } else {
497             # try to refresh the servers if we can
498 0 0       0 if ($self->attempt_limit > $self->attempts) {
499 0         0 sleep(1);
500 0 0       0 carp("No more servers to try. Attempting to refresh server list.")
501             if $self->warnings;
502 0         0 $self->attempts($self->attempts + 1);
503 0         0 $self->servers_init(0);
504 0         0 $self->server_list;
505 0         0 return $self->_make_api_call($function, $args);
506             } else {
507 0         0 Net::Mollom::ServerListException->throw(error => "No more Mollom servers to try!");
508             }
509             }
510             } elsif( $self->{_recurse_level} < $MAX_API_TRIES ) {
511 2         6 delete $self->{_recurse_level};
512 2         61 Net::Mollom::APIException->throw(
513             error => "Error communicating with Mollom [$results->{faultCode}]: $results->{faultString}",
514             mollom_code => $results->{faultCode},
515             mollom_desc => $results->{faultString},
516             );
517             } else {
518 0         0 my $msg = qq(Tried making API call "$function" $self->{_recurse_level} times but failed.)
519             . " Giving up";
520 0         0 delete $self->{_recurse_level};
521 0         0 Net::Mollom::APIException->throw(
522             error => $msg,
523             mollom_code => $results->{faultCode},
524             mollom_desc => $results->{faultString},
525             );
526             }
527             } else {
528 4         80 $self->attempts(0);
529 4 100       18 delete $self->{_recurse_level} unless $function eq 'getServerList';
530 4         54 return $results;
531             }
532             }
533              
534             =head1 EXCEPTIONS
535              
536             Any object method can throw a L object (using L underneath).
537              
538             The following exceptions are possible:
539              
540             =head2 Net::Mollom::ServerListException
541              
542             This happens when we've exhausted the list available servers and we've reached
543             our C for getting more.
544              
545             =head2 Net::Mollom::APIException
546              
547             There was some kind of problem communicating with the Mollom service.
548             This is not a network error, but somehow we're not talking to it in a language
549             it can understand (maybe an API change or bug in Net::Mollom, etc).
550              
551             =head2 Net::Mollom::CommunicationException
552              
553             There was some kind of problem communicating with the Mollom service.
554             This could be a network error or an L error.
555              
556             =head1 AUTHOR
557              
558             Michael Peters, C<< >>
559              
560             =head1 BUGS
561              
562             Please report any bugs or feature requests to
563             C, or through the web interface at
564             L. I will
565             be notified, and then you'll automatically be notified of progress on
566             your bug as I make changes.
567              
568             =head1 SUPPORT
569              
570             You can find documentation for this module with the perldoc command.
571              
572             perldoc Net::Mollom
573              
574             You can also look for information at:
575              
576             =over 4
577              
578             =item * RT: CPAN's request tracker
579              
580             L
581              
582             =item * AnnoCPAN: Annotated CPAN documentation
583              
584             L
585              
586             =item * CPAN Ratings
587              
588             L
589              
590             =item * Search CPAN
591              
592             L
593              
594             =back
595              
596              
597             =head1 ACKNOWLEDGEMENTS
598              
599              
600             =head1 COPYRIGHT & LICENSE
601              
602             Copyright 2009 Michael Peters, all rights reserved.
603              
604             This program is free software; you can redistribute it and/or modify it
605             under the same terms as Perl itself.
606              
607              
608             =cut
609              
610             1; # End of Net::Mollom