File Coverage

blib/lib/POE/Component/SmokeBox/Uploads/RSS.pm
Criterion Covered Total %
statement 12 12 100.0
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 16 16 100.0


line stmt bran cond sub pod time code
1             package POE::Component::SmokeBox::Uploads::RSS;
2              
3 1     1   48323 use strict;
  1         1  
  1         22  
4 1     1   3 use warnings;
  1         2  
  1         20  
5 1     1   3 use Carp;
  1         10  
  1         50  
6 1     1   480 use POE qw(Component::RSSAggregator Component::Client::HTTP);
  1         29949  
  1         7  
7             use HTTP::Request;
8             use HTML::LinkExtor;
9             use vars qw($VERSION);
10              
11             $VERSION = '1.02';
12              
13             sub spawn {
14             my $package = shift;
15             my %opts = @_;
16             $opts{lc $_} = delete $opts{$_} for keys %opts;
17             croak "$package requires an 'event' argument\n" unless $opts{event};
18             $opts{feed} = 'http://search.cpan.org/uploads.rdf' unless $opts{feed};
19             $opts{name} = 'search-cpan-recent' unless $opts{name};
20             $opts{delay} = 1800 unless $opts{delay};
21             my $options = delete $opts{options};
22             my $self = bless \%opts, $package;
23             $self->{session_id} = POE::Session->create(
24             object_states => [
25             $self => { shutdown => '_shutdown', },
26             $self => [ qw(_start _dispatch _feed_url _handle_feed _real_shutdown) ],
27             ],
28             heap => $self,
29             ( ref($options) eq 'HASH' ? ( options => $options ) : () ),
30             )->ID();
31             return $self;
32             }
33              
34             sub session_id {
35             return $_[0]->{session_id};
36             }
37              
38             sub shutdown {
39             my $self = shift;
40             $poe_kernel->post( $self->{session_id}, 'shutdown' );
41             return;
42             }
43              
44             sub _shutdown {
45             my ($kernel,$self) = @_[KERNEL,OBJECT];
46             $self->{_shutdown} = 1;
47             return if $self->{_http_requests};
48             $kernel->yield( '_real_shutdown' );
49             return;
50             }
51              
52             sub _real_shutdown {
53             my ($kernel,$self) = @_[KERNEL,OBJECT];
54             $kernel->alias_remove( $_ ) for $kernel->alias_list();
55             $kernel->refcount_decrement( $self->{session_id}, __PACKAGE__ ) unless $self->{alias};
56             $kernel->refcount_decrement( $self->{sender_id}, __PACKAGE__ );
57             $kernel->post( $self->{http_id}, 'shutdown' ) unless $self->{http_alias};
58             $kernel->post( $self->{rssagg}, 'shutdown' );
59             return;
60             }
61              
62             sub _start {
63             my ($kernel,$session,$sender,$self) = @_[KERNEL,SESSION,SENDER,OBJECT];
64             $self->{session_id} = $session->ID();
65             if ( $kernel == $sender and !$self->{session} ) {
66             croak "Not called from another POE session and 'session' wasn't set\n";
67             }
68             my $sender_id;
69             if ( $self->{session} ) {
70             if ( my $ref = $kernel->alias_resolve( $self->{session} ) ) {
71             $sender_id = $ref->ID();
72             }
73             else {
74             croak "Could not resolve 'session' to a valid POE session\n";
75             }
76             }
77             else {
78             $sender_id = $sender->ID();
79             }
80             $kernel->refcount_increment( $sender_id, __PACKAGE__ );
81             $self->{sender_id} = $sender_id;
82             if ( $self->{http_alias} ) {
83             my $http_ref = $kernel->alias_resolve( $self->{http_alias} );
84             $self->{http_id} = $http_ref->ID() if $http_ref;
85             }
86             unless ( $self->{http_id} ) {
87             $self->{http_id} = 'smokeboxrss' . $$ . $self->{session_id};
88             POE::Component::Client::HTTP->spawn(
89             Alias => $self->{http_id},
90             FollowRedirects => 2,
91             Timeout => 60,
92             Agent => 'Mozilla/5.0 (X11; U; Linux i686; en-US; '
93             . 'rv:1.1) Gecko/20020913 Debian/1.1-1',
94             );
95             }
96             $self->{rssagg} = 'rssagg' . $self->{session_id};
97             POE::Component::RSSAggregator->new(
98             alias => $self->{rssagg},
99             callback => $session->postback('_handle_feed'),
100             http_alias => $self->{http_id},
101             tmpdir => $self->{tmpdir} || '.', # optional caching
102             );
103             my $feed = {
104             url => $self->{feed},
105             name => $self->{name},
106             delay => $self->{delay},
107             };
108             $kernel->post( $self->{rssagg}, 'add_feed', $feed );
109             return;
110             }
111              
112             sub _handle_feed {
113             my ($kernel,$self,$feed) = (@_[KERNEL,OBJECT], $_[ARG1]->[0]);
114             for my $headline ( reverse $feed->late_breaking_news ) {
115             $kernel->post(
116             $self->{http_id},
117             'request',
118             '_feed_url',
119             HTTP::Request->new( GET => $headline->url ),
120             $headline->headline,
121             );
122             $self->{_http_requests}++;
123             }
124             return;
125             }
126              
127             sub _feed_url {
128             my ($kernel,$self,$request_packet,$response_packet) = @_[KERNEL,OBJECT,ARG0,ARG1];
129             my $http_resp = $response_packet->[0];
130             $self->{_http_requests}--;
131             return unless $http_resp and $http_resp->code() == 200;
132             my $tag = $request_packet->[1];
133             my $p = HTML::LinkExtor->new();
134             $p->parse( $http_resp->content() );
135             foreach my $link ( $p->links() ) {
136             if ( $link->[0] eq 'a' and $link->[2] =~ /\Q$tag\E/ ) {
137             ( my $module = $link->[2] ) =~ s#/CPAN/authors/id/##;
138             $kernel->call( $self->{session_id}, '_dispatch', $module );
139             last;
140             }
141             }
142             $kernel->yield( '_real_shutdown' ) if $self->{_shutdown} and $self->{_http_requests} == 0;
143             return;
144             }
145              
146             sub _dispatch {
147             my ($kernel,$self,$module) = @_[KERNEL,OBJECT,ARG0];
148             $kernel->post( $self->{sender_id}, $self->{event}, $module );
149             return;
150             }
151              
152             "This town ain't big enough for the both of us";
153              
154             __END__
155              
156             =head1 NAME
157              
158             POE::Component::SmokeBox::Uploads::RSS - Obtain uploaded CPAN modules via RSS.
159              
160             =head1 SYNOPSIS
161              
162             use strict;
163             use POE qw(Component::SmokeBox::Uploads::RSS);
164              
165             $|=1;
166              
167             POE::Session->create(
168             package_states => [
169             'main' => [qw(_start upload)],
170             ],
171             );
172              
173             $poe_kernel->run();
174             exit 0;
175              
176             sub _start {
177             POE::Component::SmokeBox::Uploads::RSS->spawn(
178             event => 'upload',
179             );
180             return;
181             }
182              
183             sub upload {
184             print $_[ARG0], "\n";
185             return;
186             }
187              
188             =head1 DESCRIPTION
189              
190             POE::Component::SmokeBox::Uploads::RSS is a L<POE> component that alerts newly uploaded CPAN
191             distributions. It obtains this information from polling an RSS feed ( by default L<http://search.cpan.org/uploads.rdf>.
192              
193             L<POE::Component::RSSAggregator> is used to handle the RSS feed monitoring and L<POE::Component::Client::HTTP> used to obtain the full author path for each new upload.
194              
195             =head1 CONSTRUCTOR
196              
197             =over
198              
199             =item C<spawn>
200              
201             Takes a number of parameters:
202              
203             'event', the event handler in your session where each new upload alert should be sent, mandatory;
204             'session', optional if the poco is spawned from within another session;
205            
206             The 'session' parameter is only required if you wish the output event to go to a different
207             session than the calling session, or if you have spawned the poco outside of a session.
208              
209             Returns an object.
210              
211             =back
212              
213             =head1 METHODS
214              
215             =over
216              
217             =item C<session_id>
218              
219             Returns the POE::Session ID of the component.
220              
221             =item C<shutdown>
222              
223             Terminates the component.
224              
225             =back
226              
227             =head1 INPUT EVENTS
228              
229             =over
230              
231             =item C<shutdown>
232              
233             Terminates the component.
234              
235             =back
236              
237             =head1 OUTPUT EVENTS
238              
239             An event will be triggered for each new CPAN upload. The event will have ARG0 set to the path of the
240             upload:
241              
242             B/BI/BINGOS/POE-Component-SmokeBox-Uploads-RSS-0.01.tar.gz
243              
244             Suitable for feeding to the smoke tester of your choice.
245              
246             =head1 AUTHOR
247              
248             Chris C<BinGOs> Williams <chris@bingosnet.co.uk>
249              
250             =head1 LICENSE
251              
252             Copyright E<copy> Chris Williams
253              
254             This module may be used, modified, and distributed under the same terms as Perl itself. Please see the license that came with your Perl distribution for details.
255              
256             =head1 SEE ALSO
257              
258             L<POE>
259              
260             L<POE::Component::RSSAggregator>
261              
262             L<POE::Component::Client::HTTP>
263              
264             L<http://search.cpan.org/uploads.rdf>
265              
266             =cut