package WebService::Google::Reader; use strict; use warnings; use base qw( Class::Accessor::Fast ); use HTTP::Cookies; use HTTP::Request::Common qw( GET POST ); use LWP::UserAgent; use JSON::Any; use URI::Escape; use URI::QueryParam; use WebService::Google::Reader::Constants; use WebService::Google::Reader::Feed; use WebService::Google::Reader::ListElement; our $VERSION = '0.07'; if ( DEBUG ) { require Carp; @SIG{qw( __DIE__ __WARN__ )} = \( &Carp::confess, &Carp::cluck ); } __PACKAGE__->mk_accessors(qw( error password scheme token ua username )); sub new { my ($class, %params) = @_; my $self = bless \%params, $class; my $ua = $params{ua}; unless ( ref $ua and $ua->isa( q(LWP::UserAgent) ) ) { $ua = LWP::UserAgent->new( # Google only compresses content for certain agents or if gzip # is part of the agent name. agent => __PACKAGE__.'/'.$VERSION . ( HAS_ZLIB ? ' (gzip)' :'' ) ); $self->ua( $ua ); } unless ( $ua->cookie_jar ) { $ua->cookie_jar( HTTP::Cookies->new( hide_cookie2 => 1 ) ); } $self->scheme( $params{secure} || $params{https} ? 'https' : 'http' ); return $self; } ## Feeds sub feed { return shift->_feed( feed => shift, @_ ); } sub tag { return shift->_feed( tag => shift, @_ ); } sub state { return shift->_feed( state => shift, @_ ); } sub shared { return shift->state( 'broadcast', @_ ); } sub starred { return shift->state( 'starred', @_ ); } sub unread { return shift->state( 'reading-list', exclude => { state => 'read' }, @_ ); } sub search { my ($self, $query, %params) = @_; $self->_login or return; $self->_token or return; my $uri = URI->new( SEARCH_IDS_URL ); my %fields; $fields{num} = $params{results} || 1000; my @types = grep { exists $params{ $_ } } qw( feed state tag ); for my $type (@types) { push @{ $fields{s} }, _encode_type( $type, $params{$type} ); } $uri->query_form( { q => $query, %fields, output => 'json' } ); my $req = HTTP::Request->new( GET => $uri ); my $res = $self->_request( $req ) or return; my @ids = do { my $ref = eval { JSON::Any->decode( $res->decoded_content ) }; if ( $@ ) { $self->error( "Failed to parse JSON response: $@" ); return; } map { $_->{id} } @{ $ref->{results} }; }; return unless @ids; if ( my $order = $params{order} || $params{sort} ) { @ids = reverse @ids if 'asc' eq $order; } my $feed = ( __PACKAGE__.'::Feed' )->new( request => $req, ids => \@ids, count => $params{count} || 40, ); return $self->more( $feed ); } sub more { my ($self, $feed) = @_; my $req; if ( defined $feed->ids ) { my @ids = splice @{ $feed->ids }, 0, $feed->count; return unless @ids; my $uri = URI->new( STREAM_IDS_CONTENT_URL, 'https' ); $req = POST( $uri, [ ( map { ('i', $_) } @ids ), T => $self->token ] ); } elsif ( $feed->elem ) { return unless defined $feed->continuation and $feed->entries; $req = $feed->request; $req->uri->query_param( c => $feed->continuation ); } elsif ( $req = $feed->request ) { # Initial request. } else { return } my $res = $self->_request( $req ) or return; $feed->init( Stream => $res->decoded_content( ref => 1 ) ) or return; return $feed; } *previous = *next = \&more; ## Lists sub tags { return $_[0]->_list( LIST_TAGS_URL ); } sub feeds { return $_[0]->_list( LIST_SUBS_URL ); } sub preferences { return $_[0]->_list( LIST_PREFS_URL ); } sub counts { return $_[0]->_list( LIST_COUNTS_URL ); } sub userinfo { my ($self) = @_; return $_[0]->_list( LIST_USER_INFO_URL ); } ## Edit tags sub edit_tag { return shift->_edit_tag( tag => @_ ); } sub edit_state { return shift->_edit_tag( state => @_ ); } sub share_tag { return shift->edit_tag( \@_, share => 1 ); } sub unshare_tag { return shift->edit_tag( \@_, unshare => 1 ); } sub share_state { return shift->edit_state( \@_, share => 1 ); } sub unshare_state { return shift->edit_state( \@_, unshare => 1 ); } sub delete_tag { return shift->edit_tag( \@_, delete => 1 ); } sub mark_read_tag { return shift->mark_read( tag => \@_ ); } sub mark_read_state { return shift->mark_read( state => \@_ ); } sub rename_feed_tag { my ($self, $old, $new) = @_; my @tagged; my @feeds = $self->feeds or return; # Get the list of subs which are associated with the tag to be renamed. FEED: for my $feed ( @feeds ) { for my $cat ( $self->categories ) { for my $o ( 'ARRAY' eq ref $old ? @$old : ( $old ) ) { if ( $old eq $cat->label or $old eq $cat->id ) { push @tagged, $feed->id; next FEED; } } } } $_ = [ _encode_type( tag => $_) ] for ( $old, $new ); return $self->edit_feed( \@tagged, tag => $new, untag => $old ); } sub rename_entry_tag { my ($self, $old, $new) = @_; for my $o ( 'ARRAY' eq ref $old ? @$old : ( $old ) ) { my $feed = $self->tag( $o ) or return; do { $self->edit_entry( [ $feed->entries ], tag => $new, untag => $old ) or return; } while ( $self->feed( $feed ) ); } return 1; } sub rename_tag { my $self = shift; return unless $self->rename_tag_feed( @_ ); return unless $self->rename_tag_entry( @_ ); return $self->delete_tags( shift ); } ## Edit feeds sub edit_feed { my ($self, $sub, %params) = @_; $self->_login or return; $self->_token or return; my $url = EDIT_SUB_URL; my %fields; for my $s ( 'ARRAY' eq ref $sub ? @$sub : ( $sub ) ) { if ( __PACKAGE__.'::Feed' eq ref $s ) { my $id = $s->id or next; $id =~ s[^tag:google.com,2005:reader/][]; $id =~ s[\?.*][]; push @{ $fields{s} }, $id; } else { push @{ $fields{s} }, _encode_type( feed => $s ); } } return 1 unless @{ $fields{s} || [] }; if ( defined( my $title = $params{title} ) ) { $fields{t} = $title; } if ( grep { exists $params{ $_ } } qw( subscribe add ) ) { $fields{ac} = 'subscribe'; } elsif ( grep { exists $params{ $_ } } qw( unsubscribe remove ) ) { $fields{ac} = 'unsubscribe'; } else { $fields{ac} = 'edit'; } # Add a tag or state. for my $t (qw( tag state )) { next unless exists $params{ $t }; defined( my $p = $params{ $t } ) or next; for my $a ( 'ARRAY' eq ref $p ? @$p : ( $p ) ) { push @{ $fields{a} }, _encode_type( $t => $a ); } } # Remove a tag or state. for my $t (qw( untag unstate )) { next unless exists $params{ $t }; defined( my $p = $params{ $t } ) or next; for my $d ( 'ARRAY' eq ref $p ? @$p : ( $p ) ) { push @{ $fields{r} }, _encode_type( substr( $t, 2 ) => $d ); } } return $self->_edit( $url, %fields ); } sub tag_feed { return shift->edit_feed( shift, tag => \@_ ); } sub untag_feed { return shift->edit_feed( shift, untag => \@_ ); } sub state_feed { return shift->edit_feed( shift, state => \@_ ); } sub unstate_feed { return shift->edit_feed( shift, unstate => \@_ ); } sub subscribe { return shift->edit_feed( \@_, subscribe => 1 ); } sub unsubscribe { return shift->edit_feed( \@_, unsubscribe => 1 ); } sub rename_feed { return $_[0]->edit_feed( $_[1], title => $_[2] ); } sub mark_read_feed { return shift->mark_read( feed => \@_ ); } ## Edit entries sub edit_entry { my ($self, $entry, %params) = @_; $self->_login or return; $self->_token or return; my %fields = ( ac => 'edit' ); for my $e ( 'ARRAY' eq ref $entry ? @$entry : ( $entry ) ) { push @{ $fields{i} }, $e->id; push @{ $fields{s} }, $e->stream_id; } return 1 unless @{ $fields{i} || [] }; my $url = EDIT_ENTRY_TAG_URL; # Add a tag or state. for my $t (qw( tag state )) { next unless exists $params{ $t }; defined( my $p = $params{ $t } ) or next; for my $a ( 'ARRAY' eq ref $p ? @$p : ( $p ) ) { push @{ $fields{a} }, _encode_type( $t => $a ); } } # Remove a tag or state. for my $t (qw( untag unstate )) { next unless exists $params{ $t }; defined( my $p = $params{ $t } ) or next; for my $d ( 'ARRAY' eq ref $p ? @$p : ( $p ) ) { push @{ $fields{r} }, _encode_type( substr( $t, 2 ) => $d ); } } return $self->_edit( $url, %fields ); } sub tag_entry { return shift->edit_entry( shift, tag => \@_ ); } sub untag_entry { return shift->edit_entry( shift, untag => \@_ ); } sub state_entry { return shift->edit_entry( shift, state => \@_ ); } sub unstate_entry { return shift->edit_entry( shift, unstate => \@_ ); } sub share_entry { return shift->edit_entry( shift, state => 'broadcast' ); } sub unshare_entry { return shift->edit_entry( shift, unstate => 'broadcast' ); } sub star_entry { return shift->edit_entry( shift, state => 'starred' ); } *star = \&star_entry; sub unstar_entry { return shift->edit_entry( shift, unstate => 'starred' ); } *unstar = \&unstar_entry; sub mark_read_entry { return shift->edit_entry( \@_, state => 'read' ); } ## Miscellaneous sub mark_read { my ($self, %params) = @_; $self->_login or return; $self->_token or return; my %fields; my @types = grep { exists $params{ $_ } } qw( feed state tag ); for my $type (@types) { push @{ $fields{s} }, _encode_type( $type, $params{$type} ); } return $self->_edit( EDIT_MARK_READ_URL, %fields ); } sub edit_preference { my ($self, $key, $val) = @_; $self->_login or return; $self->_token or return; return $self->_edit( EDIT_PREF_URL, k => $key, v => $val ); } sub opml { my ($self) = @_; $self->_login or return; my $req = GET( EXPORT_SUBS_URL ); my $res = $self->_request( $req ) or return; return $res->decoded_content; } sub ping { my ($self, %fields) = @_; my $res = $self->_request( GET( PING_URL ) ) or return; return 1 if 'OK' eq $res->decoded_content; $self->error( 'Ping failed: '. $res->decoded_content ); return; } ## Private interface sub _login { my ($self, $force) = @_; return if $self->_public; return 1 if not $force and $self->_cookie; my $uri = URI->new( LOGIN_URL ); $uri->query_form( service => 'reader', Email => $self->username, Passwd => $self->password, source => $self->ua->agent, continue => READER_URL, ); my $res = $self->ua->post( $uri ); my $content = $res->decoded_content; if ( $res->is_error ) { my ($err) = $content =~ m[ ^Error=(.*)$ ]mx; $self->error( $res->status_line . ( $err ? (': '. $err) : '' ) ); return; } my ($sid) = $content =~ m[ ^SID=(.*)$ ]mx; unless ($sid) { $self->error( 'could not find SID value for cookie' ); return; } $self->ua->cookie_jar->set_cookie( 0, SID => $sid, '/', '.google.com', undef, 1, 0, 160000000000 ); return 1; } sub _request { my ($self, $req, $count) = @_; return if $count and 2 <= $count; # Assume all POST requests are secure. $req->uri->scheme( $self->scheme ) if 'GET' eq $req->method; $req->uri->query_param( ck => time * 1000 ); $req->uri->query_param( client => $self->ua->agent ); print $req->as_string, "-"x80, "\n" if DEBUG; if ( HAS_ZLIB ) { $req->header( accept_encoding => 'gzip,deflate' ); # Doesn't always work; gets 415- unsupported media type for some urls. #if ( my $content = $req->content ) { # if ( $content = Compress::Zlib::memGzip( $content ) ) { # $req->content( $content ); # $req->content_length( length $content ); # $req->content_encoding( 'gzip' ); # } #} } my $res = $self->ua->request( $req ); if ( $res->is_error ) { # Need a fresh token. if ( $res->header( 'X-Reader-Google-Bad-Token' ) ) { print "Stale token- retrying\n" if DEBUG; $self->_token( 1 ) or return; return $self->_request( $req, $count++ ); } $self->error( $res->status_line . ' - ' . $res->decoded_content ); return; } return $res; } # NOTE: any request that sends the token, should use https. sub _token { my ($self, $force) = @_; return 1 if $self->token and not $force; $self->_login or return; my $uri = URI->new( TOKEN_URL, 'https' ); my $res = $self->_request( GET( $uri ) ) or return; return $self->token( $res->decoded_content ); } sub _public { return not $_[0]->username or not $_[0]->password; } sub _cookie { # ick, HTTP::Cookies doesn't provide an accessor. return $_[0]->ua->cookie_jar->{COOKIES}{'.google.com'}{'/'}{SID}; } sub _encode_type { my ($type, $val, $escape) = @_; my @paths; if ( 'feed' eq $type ) { @paths = _encode_feed( $val, $escape); } elsif ( 'tag' eq $type ) { @paths = _encode_tag( $val ); } elsif ( 'state' eq $type ) { @paths = _encode_state( $val ); } elsif ( 'entry' eq $type ) { @paths = _encode_entry( $val ); } else { return; } return wantarray ? @paths : shift @paths; } sub _encode_feed { my ($feed, $escape) = @_; my @paths; for my $f ( 'ARRAY' eq ref $feed ? @$feed : ( $feed ) ) { my $path = $f; if ( 'feed/' ne substr $f, 0, 5 ) { $path = 'feed/' . $escape ? uri_escape( $f ) : $f; } push @paths, $path; } return @paths; } sub _encode_tag { my ($tag) = @_; my @paths; for my $t ( 'ARRAY' eq ref $tag ? @$tag : ( $tag ) ) { my $path = $t; if ( $t !~ m[ ^user/(?:-|\d{20})/ ]x ) { $path = "user/-/label/$t" } push @paths, $path; } return @paths; } sub _encode_state { my ($state) = @_; my @paths; for my $s ( 'ARRAY' eq ref $state ? @$state : ( $state ) ) { my $path = $s; if ( $s !~ m[ ^user/(?:-|\d{20})/ ]x ) { $path = "user/-/state/com.google/$s"; } push @paths, $path; } return @paths; } sub _encode_entry { my ($entry) = @_; my @paths; for my $e ( 'ARRAY' eq ref $entry ? @$entry : ( $entry ) ) { my $path = $e; if ( 'tag:google.com,2005:reader/item/' ne substr $e, 0, 32 ) { $path = "tag:google.com,2005:reader/item/$e"; } push @paths, $path; } return @paths; } sub _feed { my ($self, $type, $val, %params) = @_; $self->_login or return; my $path = $self->_public ? ATOM_PUBLIC_URL : ATOM_URL; my $uri = URI->new( $path . _encode_type( $type, $val, 1 ) ); my %fields; if ( my $count = $params{count} ) { $fields{n} = $count; } if ( my $start_time = $params{start_time} ) { $fields{ot} = $start_time; } if ( my $order = $params{order} || $params{sort} ) { # m = magic/auto; not really sure what that is $fields{r} = 'desc' eq $order ? 'n' : 'asc' eq $order ? 'o' : $order; } if ( defined( my $continuation = $params{continuation} ) ) { $fields{c} = $continuation; } if ( my $ex = $params{exclude} ) { for my $x ( 'ARRAY' eq ref $ex ? @$ex : ( $ex ) ) { while ( my ($xtype, $exclude) = each %$x ) { push @{ $fields{xt} }, _encode_type( $xtype, $exclude ); } } } $uri->query_form( \%fields ); my $feed = ( __PACKAGE__.'::Feed' )->new( request => GET( $uri ) ); return $self->more( $feed ); } sub _list { my ($self, $url) = @_; $self->_login or return; my $uri = URI->new( $url ); $uri->query_form( { $uri->query_form, output => 'json' } ); my $res = $self->_request( GET( $uri ) ) or return; my $ref = eval { JSON::Any->decode( $res->decoded_content ) }; if ( $@ ) { $self->error( "Failed to parse JSON response: $@" ); return; } # Remove an unecessary level of indirection. my $aref = ( grep { 'ARRAY' eq ref } values %$ref )[0] || []; for my $ref ( @$aref ) { $ref = ( __PACKAGE__.'::ListElement' )->new( $ref ) } return @$aref } sub _edit { my ($self, $url, %fields) = @_; my $uri = URI->new( $url, 'https' ); my $req = POST( $uri, [ %fields, T => $self->token ] ); my $res = $self->_request( $req ) or return; return 1 if 'OK' eq $res->decoded_content; # TODO: is there a standard error format which can be reliably parsed? $self->error( 'Edit failed: '. $res->decoded_content ); return; } sub _edit_tag { my ($self, $type, $tag, %params) = @_; $self->_login or return; $self->_token or return; my %fields; push @{ $fields{s} }, _encode_type( $type => $tag ); return 1 unless @{ $fields{s} || [] }; my $url; if ( grep { exists $params{ $_ } } qw( share public ) ) { $url = EDIT_TAG_SHARE_URL; $fields{pub} = 'true'; } elsif ( grep { exists $params{ $_ } } qw( unshare private ) ) { $url = EDIT_TAG_SHARE_URL; $fields{pub} = 'false'; } elsif ( grep { exists $params{ $_ } } qw( disable delete ) ) { $url = EDIT_TAG_DISABLE_URL; $fields{ac} = 'disable-tags'; } else { $self->error( 'Unknown action' ); return; } return $self->_edit( $url, %fields ); } sub _states { return qw( read kept-unread fresh starred broadcast reading-list tracking-body-link-used tracking-emailed tracking-item-link-used tracking-kept-unread ); } 1; __END__ =head1 NAME WebService::Google::Reader - Perl interface to Google Reader =head1 SYNOPSIS use WebService::Google::Reader; my $reader = WebService::Google::Reader->new( username => $user, password => $pass, ); my $feed = $reader->unread( count => 100 ); my @entries = $feed->entries; # Fetch past entries. while ( $reader->more( $feed ) ) { my @entries = $feed->entries; } =head1 DESCRIPTION The C module provides an interface to the Google Reader service through the unofficial (as-yet unpublished) API. =head1 METHODS =over =item $reader = WebService::Google::Reader->B Creates a new WebService::Google::Reader object. The following named parameters are accepted: =over =item B and B Required for accessing any personalized or account-related functionality (reading-list, editing, etc.). =item B / B Use https scheme for all requests, even when not required. =item B An optional useragent object. =back =item $error = $reader->B Returns the error, if one occurred. =back =head2 Feed generators The following methods request an ATOM feed and return a subclass of C. These methods accept the following optional named parameters: =over =over =item B / B The sort order of the entries: B (default) or B in time. When ordering by B, Google only returns entries within 30 days, whereas the default order has no limitation. =item B Request entries only newer than this time (represented as a unix timestamp). =item B( feed => $feed|[@feeds], tag => $tag|[@tags] ) Accepts a hash reference to one or more of feed / tag / state. Each of which is a scalar or array reference. =back =back =over =item B( $feed ) Accepts a single feed url. =item B( $tag ) Accepts a single tag name. See L =item B( $state ) Accepts a single state name. See L. =item B Shortcut for B( 'broadcast' ). =item B Shortcut for B( 'starred' ). =item B Shortcut for B( 'reading-list', exclude => { state => 'read' } ) =back =over =item B( $query, %params ) Accepts a query string and the following named parameters: =over =item B / B / B One or more (as a array reference) feed / state / tag to search. The default is to search all feed subscriptions. =item B The total number of search results: defaults to 1000. =item B The number of entries per fetch: defaults to 40. =item B / B The sort order of the entries: B (default) or B in time. =back =item B / B / B A feed generator only returns B<$count> entries. If more are available, calling this method will return a feed with the next B<$count> entries. =back =head2 List generators The following methods return an object of type C. =over =item B Returns a list of subscriptions and a count of unread entries. Also listed are any tags or states which have positive unread counts. The following accessors are provided: id, count. The maximum count reported is 1000. =item B Returns the list of user subscriptions. The following accessors are provided: id, title, categories, firstitemmsec. categories is a reference to a list of Cs providing accessors: id, label. =item B Returns the list of preference settings. The following accessors are provided: id, value. =item B Returns the list of user-created tags. The following accessors are provided: id, shared. =item B Returns the list of user information. The following accessors are provided: isBloggerUser, userId, userEmail. =back =head2 Edit feeds The following methods are used to edit feed subscriptions. =over =item B( $feed|[@feeds], %params ) Requires a feed url or Feed object, or a reference to a list of them. The following named parameters are accepted: =over =item B / B Flag indicating whether the target feeds should be added or removed from the user's subscriptions. =item B Accepts a title to associate with the feed. This probaby wouldn't make sense to use when there are multiple feeds. (Maybe later will consider allowing a list here and zipping the feed and title lists). =item B<tag> / B<state> / B<untag> / B<unstate> Accepts a tag / state or a reference to a list of tags / states for which to associate / unassociate the target feeds. =back =item B<tag_feed>( $feed|[@feeds], @tags ) =item B<untag_feed>( $feed|[@feeds], @tags ) =item B<state_feed>( $feed|[@feeds], @states ) =item B<unstate_feed>( $feed|[@feeds], @states ) Associate / unassociate a list of tags / states from a feed / feeds. =item B<subscribe>( @feeds ) =item B<unsubscribe>( @feeds ) Subscribe or unsubscribe from a list of feeds. =item B<rename_feed>( $feed|[@feeds], $title ) Renames a feed to the given title. =item B<mark_read_feed>( @feeds ) Marks the feeds as read. =back =head2 Edit tags / states The following methods are used to edit tags and states. =over =item B<edit_tag>( $tag|[@tags], %params ) =item B<edit_state>( $state|[@states], %params ) Accepts the following parameters. =over =item B<share> / B<public> Make the given tags / states public. =item B<unshare> / B<private> Make the given tags / states private. =item B<disable> / B<delete> Only tags (and not states) can be disabled. =back =item B<share_tag>( @tags ) =item B<unshare_tag>( @tags ) =item B<share_state>( @states ) =item B<unshare_state>( @states ) Associate / unassociate the 'broadcast' state with the given tags / states. =item B<delete_tag>( @tags ) Delete the given tags. =item B<rename_feed_tag>( $oldtag|[@oldtags], $newtag|[@newtags] Renames the tags associated with any feeds. =item B<rename_entry_tag>( $oldtag|[@oldtags], $newtag|[@newtags] Renames the tags associated with any individual entries. =item B<rename_tag>( $oldtag|[@oldtags], $newtag|[@newtags] Calls B<rename_feed_tag> and B<rename_entry_tag>, and finally B<delete_tag>. =item B<mark_read_tag>( @tags ) =item B<mark_read_state>( @states ) Marks all entries as read for the given tags / states. =back =head2 Edit entries The following methods are used to edit individual entries. =over =item B<edit_entry>( $entry|[@entries], %params ) =over =item B<tag> / B<state> / B<untag> / B<unstate> Associate / unassociate the entries with the given tags / states. =back =item B<tag_entry>( $entry|[@entries], @tags ) =item B<untag_entry>( $entry|[@entries], @tags ) =item B<state_entry>( $entry|[@entries], @tags ) =item B<unstate_entry>( $entry|[@entries], @tags ) Associate / unassociate the entries with the given tags / states. =item B<share_entry>( @entries ) =item B<unshare_entry>( @entries ) Marks all the given entries as "broadcast". =item B<star> =item B<star_entry> =item B<unstar> =item B<unstar_entry> Marks / unmarks all the given entries as "starred". =item B<mark_read_entry>( @entries ) Marks all the given entries as "read". =back =head2 Miscellaneous These are a list of other useful methods. =over =item B<edit_preference>( $key, $value ) Sets the given preference name to the given value. =item B<mark_read>( feed => $feed|[@feeds], state => $state|[@states], tag => $tag|[@tags] ) =item B<opml> Exports feed subscriptions as OPML. =item B<ping> Returns true / false on success / failure. Unsure of when this needs to be used. =back =head2 Private methods The following private methods may be of use to others. =over =item B<_login> This is automatically called from within methods that require authorization. An optional parameter is accepted which when true, will force a login even if a previous login was successful. The end result of a successful login is to set the SID cookie. =item B<_request> Given an C<HTTP::Request>, this will perform the request and if the response indicates a bad (expired) token, it will request another token before performing the request again. Returns an C<HTTP::Response> on success, false on failure (check B<error>). =item B<_token> This is automatically called from within methods that require a user token. If successful, the token is available via the B<token> accessor. =item B<_states> Returns a list of all the known states. See L</STATES>. =back =head1 TAGS The following characters are not allowed: "E<lt>E<gt>?&/\^ =head1 STATES These are tags in a Google-specific namespace. The following are all the known used states. =over =item read Entries which have been read. =item kept-unread Entries which have been read, but marked unread. =item fresh New entries from reading-list. =item starred Entries which have been starred. =item broadcast Entries which have been shared and made publicly available. =item reading-list Entries from all subscriptions. =item tracking-body-link-used Entries for which a link in the body has been clicked. =item tracking-emailed Entries which have been mailed. =item tracking-item-link-used Entries for which the title link has been clicked. =item tracking-kept-unread Entries which have been kept unread. (Not sure how this differs from "kept-unread"). =back =head1 NOTES If C<Compress::Zlib> is found, then requests will accept compressed responses. =head1 SEE ALSO L<XML::Atom::Feed> L<http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI> =head1 REQUESTS AND BUGS Please report any bugs or feature requests to L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=WebService-Google-Reader>. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. =head1 SUPPORT You can find documentation for this module with the perldoc command. perldoc WebService::Google::Reader You can also look for information at: =over =item * AnnoCPAN: Annotated CPAN documentation L<http://annocpan.org/dist/WebService-Google-Reader> =item * CPAN Ratings L<http://cpanratings.perl.org/d/WebService-Google-Reader> =item * RT: CPAN's request tracker L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=WebService-Google-Reader> =item * Search CPAN L<http://search.cpan.org/dist/WebService-Google-Reader> =back =head1 COPYRIGHT AND LICENSE Copyright (C) 2007 gray <gray at cpan.org>, all rights reserved. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 AUTHOR gray, <gray at cpan.org> =cut