package POE::Component::WWW::PAUSE::RecentUploads::Tail; use warnings; use strict; our $VERSION = '0.05'; use Carp; use Storable; use POE qw(Component::WWW::PAUSE::RecentUploads); sub spawn { my $class = shift; croak "Must have even number of arguments to $class" if @_ & 1; my %params = @_; $params{ lc $_ } = delete $params{ $_ } for keys %params; delete $params{options} unless ref $params{options} eq 'HASH'; for ( qw(login pass) ) { croak "Missing `$_` mandatory parameter" unless exists $params{ $_ }; } unless ( $params{store} ) { warn "Missing `store` parameter\n" if $params{debug}; $params{store} = 'pause_recent.data'; } unless ( $params{ua_args} ) { $params{ua_args}{timeout} = 30; } eval { $params{recent_data} = retrieve $params{store}; }; if ( $@ and $params{debug} ) { warn "Failed to load data from `$params{store}`. This is" . " normal if the file didn't exist before\n"; } my $self = bless \%params, $class; $self->{recent_poco} = POE::Component::WWW::PAUSE::RecentUploads->spawn( login => $self->{login}, pass => $self->{pass}, ua_args => $self->{ua_args}, debug => exists $self->{debug} ? $self->{debug} : 0, ); $self->{session_id} = POE::Session->create( object_states => [ $self => { fetch => '_fetch', shutdown => '_shutdown', stop_interval => '_stop_interval', }, $self => [ qw( _got_recent _ask_recent _start ) ], ], ( exists $params{options} ? ( options => $params{options} ) : () ), )->ID; return $self; } sub _start { my ( $kernel, $self ) = @_[ KERNEL, OBJECT ]; $self->{session_id} = $_[SESSION]->ID(); if ( $self->{alias} ) { $kernel->alias_set( $self->{alias} ); } else { $kernel->refcount_increment( $self->{session_id} => __PACKAGE__ ); } } sub shutdown { my $self = shift; $poe_kernel->post( $self->{session_id} => 'shutdown' ); } sub _shutdown { my ( $kernel, $self ) = @_[ KERNEL, OBJECT ]; return if $self->{shutdown}; $self->_kill_tails; $poe_kernel->delay('_ask_recent'); $self->{recent_poco}->shutdown; $kernel->alarm_remove_all; $kernel->alias_remove( $_ ) for $kernel->alias_list; $self->{flag} = 1; $kernel->refcount_decrement( $self->{session_id} => __PACKAGE__ ) unless $self->{alias}; $self->{shutdown} = 1; } sub session_id { return $_[0]->{session_id}; } sub stop_interval { my $self = shift; $poe_kernel->yield( $self->{session_id} => 'stop_interval' => @_ ); } sub _stop_interval { my ( $kernel, $self, $req_id ) = @_[ KERNEL, OBJECT, ARG0 ]; my $flag = $self->{flags}{ $req_id }; if ( $flag ) { $kernel->alarm_remove( $flag->{delay} ) if exists $flag->{delay}; $kernel->refcount_decrement( $flag->{sender} => __PACKAGE__ ); delete $self->{flags}{ $req_id }; } else { $self->_kill_tails; } } sub _kill_tails { my $self = shift; foreach my $req_id ( keys %{ $self->{flags} } ) { my $flag = $self->{flags}{ $req_id }; $poe_kernel->alarm_remove( $flag->{delay} ) if exists $flag->{delay}; $poe_kernel->refcount_decrement( $flag->{sender} => __PACKAGE__ ); } $self->{flags} = {}; } sub fetch { my $self = shift; $poe_kernel->post( $self->{session_id} => 'fetch' => @_ ); } sub _fetch { my ( $kernel, $self, $args) = @_[ KERNEL, OBJECT, ARG0 ]; my $sender = $_[SENDER]->ID; unless ( exists $args->{event} ) { warn "Missing `event` argument"; return; } if ( $args->{session} ) { if ( my $ref = $kernel->alias_resolve( $args->{session} ) ) { $args->{sender} = $ref->ID; } else { warn "Could not resolve `session` parameter to a " . "valid POE session. Aborting..."; return; } } else { $args->{sender} = $sender; } if ( exists $args->{interval} ) { $args->{interval} = 0 + $args->{interval}; } else { $args->{interval} = 1800; } my $req_id = rand() . time() . rand(); my $req_flag = { req_id => $req_id, sender => $args->{sender}, flag => 1, }; $self->{flags}{ $req_id } = $req_flag; $args->{flag} = $req_id; $kernel->refcount_increment( $args->{sender} => __PACKAGE__ ); $kernel->post( $self->{session_id} => '_ask_recent' => $args ); } sub _ask_recent { my ( $kernel, $self, $args ) = @_[ KERNEL, OBJECT, ARG0 ]; my $req_id = $args->{flag}; my $flag = $self->{flags}{ $req_id }; if ( $flag->{flag} ) { $self->{recent_poco}->fetch( { event => '_got_recent', _args => $args , } ); if ( $args->{interval} ) { $flag->{delay} = $kernel->delay( _ask_recent => $args->{interval} => $args ); } } } sub _got_recent { my ( $kernel, $self, $input ) = @_[ KERNEL, OBJECT, ARG0 ]; my $args = { %{ $input->{_args} } }; my $req_id = $args->{flag}; my $session = delete $args->{sender}; my $event = delete $args->{event}; if ( $self->{flags}{ $req_id }{flag} ) { if ( $input->{error} ) { $args->{error} = $input->{error}; } else { $args->{data} = $self->_make_recent_data( $input->{data} ); } $args->{time} = time(); $args->{req_id} = delete $args->{flag}; # 'flag' is a poor name :( $kernel->post( $session, $event, $args ); unless ( $args->{interval} ) { delete $self->{flags}{ $req_id }; $poe_kernel->refcount_decrement( $session => __PACKAGE__ ); } } undef; } sub _make_recent_data { my ( $self, $data ) = @_; my $recent_dists_ref = { map { $_->{dist} => $_ } @$data }; foreach my $stored_dist ( keys %{ $self->{recent_data} } ) { delete $self->{recent_data}{ $stored_dist } unless exists $recent_dists_ref->{ $stored_dist }; } my @new_dists; foreach my $dist ( keys %$recent_dists_ref ) { next if exists $self->{recent_data}{ $dist }; $self->{recent_data}{ $dist } = $recent_dists_ref->{ $dist }; push @new_dists, $recent_dists_ref->{ $dist }; } eval { store $self->{recent_data}, $self->{store}; }; warn "Failed to store recent data into `$self->{store}` ($@)" if $@; return \@new_dists; } 1; __END__ # Below is stub documentation for your module. You'd better edit it! =head1 NAME POE::Component::WWW::PAUSE::RecentUploads::Tail - tail recent uploads to PAUSE. =head1 SYNOPSIS use strict; use warnings; use POE qw(Component::WWW::PAUSE::RecentUploads::Tail); POE::Component::WWW::PAUSE::RecentUploads::Tail->spawn( login => 'PAUSE_LOGIN', pass => 'PAUSE_PASSWORD', store => 'data.file', # where to store old data debug => 1, alias => 'pause', ); POE::Session->create( package_states => [ main => [ qw( _start recent ) ], ], ); $poe_kernel->run; sub _start { $poe_kernel->post( pause => fetch => { event => 'recent', interval => 600, # 10 minute interval } ); } sub recent { my $data_ref = $_[ARG0]; my $iter_time = localtime $data_ref->{time}; if ( $data_ref->{error} ) { print "Failed on this itteration with $data_ref->{error}\n"; } else { my $message = @{ $data_ref->{data} } ? "\nNew uploads at $iter_time\n" : "\nNo uploads at $iter_time\n"; print $message; foreach my $dist ( @{ $data_ref->{data} } ) { printf "%s by %s (size: %s)\n", @$dist{ qw( dist name size ) }; } } } Using the event based interface is also possible, of course. =head1 DESCRIPTION This module accesses the list of recent uploads on L and reports whichever it didn't report earlier. =head1 CONSTRUCTOR my $poco = POE::Component::WWW::PAUSE::RecentUploads::Tail->spawn( login => 'PAUSE LOGIN', # mandatory pass => 'PAUSE PASSWORD', # mandatory ); POE::Component::WWW::PAUSE::RecentUploads::Tail->spawn( login => 'PAUSE LOGIN', # mandatory pass => 'PAUSE PASSWORD', # mandatory store => 'storage_file.data', # this and all the rest are optional alias => 'recent', debug => 1, ua_args => { timeout => 10, agent => 'RecentUA', # other LWP::UserAgent's constructor arguments can go here }, options => { debug => 1, # POE::Session create() may go here. }, ); Spawns a new POE::Component::WWW::PAUSE::RecentUploads::Tail component and returns a reference to it, but you don't have to keep it if you set the optional C argument. Takes a single argument which is a hashref of options. Two of them, C and C are mandatory, the rest is optional. The possible keys/values are as follows: =head2 login { login => 'PAUSE LOGIN' } B. Must contain your L login. =head2 pass { login => 'PAUSE LOGIN' } B. Must contain your L password. =head2 store { store => 'storage_file.data' } { store => '/tmp/storage_file.data' } B. Specifies the filename of the file where we are going to store the already reported modules. B C in the current directory. =head2 alias { alias => 'recent' } B. Specifies the component's L alias of the component. =head2 debug B. Enables output of some debug messages (usually not very useful). =head2 ua_args { debug => 1 } B. When set to a true value will make the component emit some debuging info. B C<0>. =head2 options { options => { trace => 1, default => 1, } } A hashref of POE Session options to pass to the component's session. =head1 METHODS These are the object-oriented methods of the component. =head2 fetch $poco->fetch( { event => 'recent' } ); $poco->fetch( { event => 'recent', # the only mandatory argument interval => 600, # 10 minute interval login => 'other_login', # this and below is optional pass => 'other_pass', session => 'other_session', ua_args => { timeout => 10, # default timeout is 30. argent => 'LolFetcher', }, _user1 => 'random', _cow => 'meow', } ); Instructs the component to fetch information about recent PAUSE uploads. See C event description below for more information. =head2 stop_interval $poco->stop_interval; $poco->stop_interval( $req_id ); The C method stops an already running request (which is started with C method/event). The output of each itteration will contain a C key (see OUTPUT section) which you may pass as an optional argument to the method. When no arguments are specified stops all currently running requests. =head2 session_id my $fetcher_id = $poco->session_id; Takes no arguments. Returns POE Session ID of the component. =head2 shutdown $poco->shutdown; Takes no arguments. Shuts the component down. =head1 ACCEPTED EVENTS The interaction with the component is also possible via event based interface. The following events are accepted by the component: =head2 fetch $poco->fetch( { event => 'recent' } ); $poco->fetch( { event => 'recent', # the only mandatory argument interval => 600, # 10 minute interval login => 'other_login', # this and below is optional pass => 'other_pass', session => 'other_session', ua_args => { timeout => 10, # default timeout is 30. argent => 'LolFetcher', }, _user1 => 'random', _cow => 'meow', } ); Takes one argument which is a hashref with the following keys: =head3 event { event => 'event_where_to_send_output' } B. The name of the event which to send when output is ready. See OUTPUT section for its format. =head3 interval { interval => 600 } B. Specifies the interval in I between requests to PAUSE for fresh list. If specified to C<0> will make the component only fire a single shot request without setting any interval. B C<1800> (30 minutes) =head3 session { session => 'other_session_alias' } { session => $other_session_ID } { session => $other_session_ref } B. Specifies an alternative POE Session to send the output to. Accepts either session alias, session ID or session reference. B the current session. =head3 login { login => 'some_other_login' } B.Using C argument you may override the PAUSE login you've specified in the constructor. B contructor's C value. =head3 pass { pass => 'some_other_password' } B. Using C argument you may override the PAUSE password you've specified in the constructor. B contructor's C value. =head3 ua_args { ua_args => { timeout => 10, # defaults to 30 agent => 'SomeUA', # the rest of LWP::UserAgent contructor arguments }, } B. The C key takes a hashref as a value which should contain the arguments which will be passed to L contructor. I all arguments will B whatever L default contructor arguments are except for the C, which will default to 30 seconds. =head3 user defined arguments { _user_var => 'foos', _another_one => 'bars', _some_other => 'beers', } B. Any keys beginning with the C<_> (underscore) will be present in the output intact. If C option (see below) is specified, any arguments will also be present in the result of "finished downloading" event. =head2 stop_interval $poe_kernel->post( recent => 'stop_interval' ); $poe_kernel->post( recent => 'stop_interval' => $req_id ); The C method stops an already running request (which is started with C method/event). The output of each itteration will contain a C key (see OUTPUT section) which you may pass as an optional argument to the method. When no arguments are specified stops all currently running requests. =head2 shutdown $poe_kernel->post( recent => 'shutdown' ); Takes no arguments, instructs the component to shut itself down. =head1 OUTPUT $VAR1 = { 'data' => [ { 'name' => 'PEVANS', 'dist' => 'Socket-GetAddrInfo-0.08_6', 'size' => '11502b' }, { 'name' => 'JROBINSON', 'dist' => 'SQL-Translator-0.0899_02', 'size' => '551090b' }, ], 'time' => 1202002776, 'req_id' => '0.27891249752520212020027750.348456874580251', 'interval' => 600, '_user_key' => 'user_data' }; The event handler set up to listen for the event, name of which you've specified in the C argument of C event/method will recieve the results in C in the form of a hashref with one or more of the keys presented below. The keys of the output hashref are as follows: =head2 data { 'data' => [ { 'name' => 'CJUKUO', 'dist' => 'AIIA-GMT-0.01', 'size' => '33428b' }, { 'name' => 'DOMQ', 'dist' => 'Alien-Selenium-0.07', 'size' => '1640987b' }, # more of these here ], } Unless an error occured, the C key will be present and the value of it will be an arrayref of hashrefs representing recent uploads to PAUSE. The keys of those hashrefs are as follows: =head3 name { 'name' => 'CJUKUO' } The author ID of the upload. I often ID will show up on PAUSE a bit later than the upload itself. If author's ID is missing the component will ignore this upload and will report it later. =head3 dist { 'dist' => 'Alien-Selenium-0.07' } The name of the distro (or file if you prefer) that was uploaded. =head3 size { 'size' => '1640987b' } The size of the uploaded file. The value will also contain the unit of measure. =head2 error { 'error' => '401 Authorization Required' } If an error occured the C key will be present and will contain the description of the error. =head2 time { 'time' => 1202002776 } my $request_time = localtime $data_ref->{time}; Will contain the output of Perl's C