├── .gitignore ├── Changes ├── README.pod ├── dist.ini ├── lib └── Google │ └── Plus.pm └── t ├── 00-load.t └── 01-plus.t /.gitignore: -------------------------------------------------------------------------------- 1 | blib* 2 | Makefile 3 | Makefile.old 4 | Build 5 | Build.bat 6 | _build* 7 | pm_to_blib* 8 | *.tar.gz 9 | .lwpcookies 10 | cover_db 11 | pod2htm*.tmp 12 | Google-Plus-* 13 | .build* 14 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Revision history for Google::Plus 2 | 3 | {{$NEXT}} 4 | 5 | 0.004 2011-10-11 6 | Refactoring release 7 | Perldoc and dist.ini minor fixes 8 | Use an HTTP/HTTPS proxy when available 9 | Support requesting partial responses 10 | 11 | 0.003 2011-09-26 12 | Add tests and fixes 13 | Remove Person (be refactored later) 14 | Add getting person's activities 15 | Add getting a specific activity 16 | Improve documentation 17 | 18 | 0.002 2011-09-18 19 | Add some docs to Google::Plus::Person 20 | Put in *alpha* software warning 21 | 22 | 0.001 2011-09-18 23 | Initial release 24 | Can retrieve profile info into Google::Plus::Person 25 | 26 | -------------------------------------------------------------------------------- /README.pod: -------------------------------------------------------------------------------- 1 | =head1 NAME 2 | 3 | Google::Plus - simple interface to Google+ 4 | 5 | =head1 SYNOPSIS 6 | 7 | use Google::Plus; 8 | use v5.10.1; 9 | 10 | my $plus = Google::Plus->new(key => $your_gplus_api_key); 11 | 12 | # get a person's profile 13 | my $user_id = '112708775709583792684'; 14 | my $person = $plus->person($user_id); 15 | say "Name: ", $person->{displayName}; 16 | 17 | # get this person's activities 18 | my $activities = $plus->activities($user_id); 19 | while ($activities->{nextPageToken}) { 20 | my $next = $activities->{nextPageToken}; 21 | for my $item (@{$activities->{items}}) { 22 | ...; 23 | } 24 | $activities = $plus->activities($user_id, $next); 25 | } 26 | 27 | # get a specific activity 28 | my $post = 'z13uxtsawqqwwbcjt04cdhsxcnfyir44xeg'; 29 | my $act = $plus->activity($post); 30 | say "Activity: ", $act->{title}; 31 | 32 | =head1 DESCRIPTION 33 | 34 | This module lets you access Google+ people profiles and activities from 35 | Perl. Currently, only access to public data is supported; authenticated 36 | requests for C and other private data will follow in a future 37 | release. 38 | 39 | This module is B software, use at your own risk. 40 | 41 | =head1 ATTRIBUTES 42 | 43 | =head2 C 44 | 45 | my $key = $plus->key; 46 | my $key = $plus->key('xxxxNEWKEYxxxx'); 47 | 48 | Google+ API key, used for retrieving content. Usually set using L. 49 | 50 | =head2 C 51 | 52 | my $ua = $plus->ua; 53 | my $ua = $plus->ua(Mojo::UserAgent->new); 54 | 55 | User agent object that retrieves JSON from the Google+ API endpoint. 56 | Defaults to a L object. This object will use 57 | HTTP/HTTPS proxies when available (via C and C 58 | environment variables.) 59 | 60 | =head1 METHODS 61 | 62 | L implements the following methods: 63 | 64 | =head2 C 65 | 66 | my $plus = Google::Plus->new(key => $google_plus_api_key); 67 | 68 | Construct a new L object. Needs a valid Google+ API key, 69 | which you can get at L. 70 | 71 | =head2 C 72 | 73 | my $person = $plus->person('userId'); 74 | my $person = $plus->person('userId', 'fields'); 75 | 76 | Get a Google+ person's public profile. Returns a L decoded 77 | hashref describing the person's profile in L format. If 79 | C is given, limit response to the specified fields; see the 80 | Partial Responses section of L. 81 | 82 | =head2 C 83 | 84 | my $acts = $plus->activities('userId'); 85 | my $acts = $plus->activities('userId', 'collection'); 86 | my $acts = $plus->activities('userId', 'collection', nextPage'); 87 | my $acts = $plus->activities('userId', 'collection', nextPage', 'fields'); 88 | 89 | Get person's list of public activities, returning a L 90 | decoded hashref describing the person's activities in L format; this method also 92 | accepts requesting partial responses if C is given. If 93 | C is given, use that as the collection of activities to 94 | list; the default is to list C activities instead. If a 95 | C token is given, this method retrieves the next page of 96 | activities this person has. 97 | 98 | =head2 C 99 | 100 | my $post = $plus->activity('activityId') 101 | my $post = $plus->activity('activityId', fields'); 102 | 103 | Get a specific activity/post. Returns a L decoded hashref 104 | describing the activity in L format. If C is 106 | given, limit response to specified fields. 107 | 108 | =head1 SEE ALSO 109 | 110 | =over 111 | 112 | =item * L 113 | 114 | =item * L 115 | 116 | =item * L 117 | 118 | =item * L 119 | 120 | =back 121 | 122 | =cut 123 | 124 | =head1 DEVELOPMENT 125 | 126 | Send pull requests to 127 | L. You can also post issues there or at L. 129 | 130 | =head1 AUTHOR 131 | 132 | Zak B. Elep, C 133 | 134 | =head1 COPYRIGHT AND LICENSE 135 | 136 | This software is Copyright (c) 2011, Zak B. Elep. 137 | 138 | This is free software, you can redistribute it and/or modify it under 139 | the same terms as Perl language system itself. 140 | 141 | =cut 142 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name = Google-Plus 2 | author = Zak B. Elep 3 | license = Perl_5 4 | copyright_holder = Zak B. Elep 5 | copyright_year = 2011 6 | 7 | [Git::NextVersion] 8 | 9 | [MetaJSON] 10 | [MetaResources] 11 | repository.url = git://github.com/zakame/perl-google-plus.git 12 | repository.web = https://github.com/zakame/perl-google-plus 13 | repository.type = git 14 | 15 | [NextRelease] 16 | format = %-8v%{yyyy-MM-dd}d 17 | 18 | [@Git] 19 | [@Basic] 20 | 21 | [AutoPrereqs] 22 | 23 | [PkgVersion] 24 | 25 | [ExtraTests] 26 | [PodCoverageTests] 27 | [PodSyntaxTests] 28 | -------------------------------------------------------------------------------- /lib/Google/Plus.pm: -------------------------------------------------------------------------------- 1 | package Google::Plus; 2 | use Mojo::Base -base; 3 | use v5.10.1; 4 | 5 | use Mojo::URL; 6 | use Mojo::UserAgent; 7 | use IO::Socket::SSL 1.37; 8 | use Carp; 9 | 10 | has [qw/key ua/]; 11 | 12 | our $service = 'https://www.googleapis.com/plus/v1'; 13 | 14 | our %API = ( 15 | person => '/people/ID', 16 | activities => '/people/ID/activities/COLLECTION', 17 | activity => '/activities/ID' 18 | ); 19 | 20 | # transform our api dispatch above into an HTTPS request 21 | # returns JSON decoded result or throws exception otherwise 22 | sub _request { 23 | my ( $self, $api, $id, $args ) = @_; 24 | 25 | my $key = $self->key; 26 | my $ua = $self->ua; 27 | 28 | $api =~ s/ID/$id/; 29 | 30 | my $url = Mojo::URL->new( join '', $service => $api ); 31 | 32 | # $args is a hashref corresponding to optional query parameters (such 33 | # as nextPageToken) 34 | if ( ref $args eq 'HASH' ) { 35 | PARAM: while ( my ( $k, $v ) = each %$args ) { 36 | $k eq 'collection' and do { 37 | my $p = $url->path->to_string; 38 | $p =~ s/COLLECTION/$v/; 39 | $url = $url->path($p); 40 | next PARAM; 41 | }; 42 | $url = $url->query( { $k => $v } ); 43 | } 44 | } 45 | $url = $url->query( { key => $key } ); 46 | 47 | my $tx = $ua->get($url); 48 | $tx->success and return $tx->res->json; 49 | 50 | # we never get here, unless something went wrong 51 | my $message = $tx->error; 52 | $tx->res->json and do { 53 | my $json_err = $tx->res->json->{error}->{message}; 54 | $message = join ' ', $message => $json_err; 55 | }; 56 | die "Error: $message"; 57 | } 58 | 59 | sub new { 60 | my $self = bless {}, shift; 61 | 62 | croak "API key required" unless $_[0] and $_[0] eq 'key'; 63 | 64 | $self->key( $_[1] ); 65 | $self->ua( Mojo::UserAgent->new->detect_proxy ); 66 | 67 | $self; 68 | } 69 | 70 | sub person { 71 | my ( $self, $user_id, $fields ) = @_; 72 | 73 | croak 'user ID required' unless $user_id; 74 | croak 'Invalid user ID' unless $user_id =~ /[0-9]+/; 75 | 76 | $fields 77 | ? $self->_request( $API{person} => $user_id, { fields => $fields } ) 78 | : $self->_request( $API{person} => $user_id ); 79 | } 80 | 81 | sub activities { 82 | my ( $self, $user_id, $collection, $next, $fields ) = @_; 83 | 84 | croak 'user ID required' unless $user_id; 85 | croak 'Invalid user ID' unless $user_id =~ /[0-9]+/; 86 | 87 | $collection //= 'public'; 88 | 89 | my %args = ( collection => $collection ); 90 | $args{pageToken} = $next if $next; 91 | $args{fields} = $fields if $fields; 92 | 93 | $self->_request( $API{activities} => $user_id, \%args ); 94 | } 95 | 96 | sub activity { 97 | my ( $self, $activity_id, $fields ) = @_; 98 | 99 | croak 'activity ID required' unless $activity_id; 100 | croak 'Invalid activity ID' unless $activity_id =~ /\w+/; 101 | 102 | $fields 103 | ? $self->_request( $API{activity} => $activity_id, { fields => $fields } ) 104 | : $self->_request( $API{activity} => $activity_id ); 105 | } 106 | 107 | "Inspired by tempire's Google::Voice :3"; 108 | 109 | __END__ 110 | 111 | =head1 NAME 112 | 113 | Google::Plus - simple interface to Google+ 114 | 115 | =head1 SYNOPSIS 116 | 117 | use Google::Plus; 118 | use v5.10.1; 119 | 120 | my $plus = Google::Plus->new(key => $your_gplus_api_key); 121 | 122 | # get a person's profile 123 | my $user_id = '112708775709583792684'; 124 | my $person = $plus->person($user_id); 125 | say "Name: ", $person->{displayName}; 126 | 127 | # get this person's activities 128 | my $activities = $plus->activities($user_id); 129 | while ($activities->{nextPageToken}) { 130 | my $next = $activities->{nextPageToken}; 131 | for my $item (@{$activities->{items}}) { 132 | ...; 133 | } 134 | $activities = $plus->activities($user_id, $next); 135 | } 136 | 137 | # get a specific activity 138 | my $post = 'z13uxtsawqqwwbcjt04cdhsxcnfyir44xeg'; 139 | my $act = $plus->activity($post); 140 | say "Activity: ", $act->{title}; 141 | 142 | =head1 DESCRIPTION 143 | 144 | This module lets you access Google+ people profiles and activities from 145 | Perl. Currently, only access to public data is supported; authenticated 146 | requests for C and other private data will follow in a future 147 | release. 148 | 149 | This module is B software, use at your own risk. 150 | 151 | =head1 ATTRIBUTES 152 | 153 | =head2 C 154 | 155 | my $key = $plus->key; 156 | my $key = $plus->key('xxxxNEWKEYxxxx'); 157 | 158 | Google+ API key, used for retrieving content. Usually set using L. 159 | 160 | =head2 C 161 | 162 | my $ua = $plus->ua; 163 | my $ua = $plus->ua(Mojo::UserAgent->new); 164 | 165 | User agent object that retrieves JSON from the Google+ API endpoint. 166 | Defaults to a L object. This object will use 167 | HTTP/HTTPS proxies when available (via C and C 168 | environment variables.) 169 | 170 | =head1 METHODS 171 | 172 | L implements the following methods: 173 | 174 | =head2 C 175 | 176 | my $plus = Google::Plus->new(key => $google_plus_api_key); 177 | 178 | Construct a new L object. Needs a valid Google+ API key, 179 | which you can get at L. 180 | 181 | =head2 C 182 | 183 | my $person = $plus->person('userId'); 184 | my $person = $plus->person('userId', 'fields'); 185 | 186 | Get a Google+ person's public profile. Returns a L decoded 187 | hashref describing the person's profile in L format. If 189 | C is given, limit response to the specified fields; see the 190 | Partial Responses section of L. 191 | 192 | =head2 C 193 | 194 | my $acts = $plus->activities('userId'); 195 | my $acts = $plus->activities('userId', 'collection'); 196 | my $acts = $plus->activities('userId', 'collection', 'nextPage'); 197 | my $acts = $plus->activities('userId', 'collection', 'nextPage', 'fields'); 198 | 199 | Get person's list of public activities, returning a L 200 | decoded hashref describing the person's activities in L format; this method also 202 | accepts requesting partial responses if C is given. If 203 | C is given, use that as the collection of activities to 204 | list; the default is to list C activities instead. If a 205 | C token is given, this method retrieves the next page of 206 | activities this person has. 207 | 208 | =head2 C 209 | 210 | my $post = $plus->activity('activityId') 211 | my $post = $plus->activity('activityId', 'fields'); 212 | 213 | Get a specific activity/post. Returns a L decoded hashref 214 | describing the activity in L format. If C is 216 | given, limit response to specified fields. 217 | 218 | =head1 SEE ALSO 219 | 220 | =over 221 | 222 | =item * L 223 | 224 | =item * L 225 | 226 | =item * L 227 | 228 | =item * L 229 | 230 | =back 231 | 232 | =cut 233 | 234 | =head1 DEVELOPMENT 235 | 236 | This project is hosted on Github, at 237 | L. Post issues to L. 239 | 240 | =head1 AUTHOR 241 | 242 | Zak B. Elep, C 243 | 244 | =head1 COPYRIGHT AND LICENSE 245 | 246 | This software is Copyright (c) 2011, Zak B. Elep. 247 | 248 | This is free software, you can redistribute it and/or modify it under 249 | the same terms as Perl language system itself. 250 | 251 | =cut 252 | -------------------------------------------------------------------------------- /t/00-load.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Test::Most 'bail'; 3 | 4 | BEGIN { 5 | plan tests => 1; 6 | use_ok 'Google::Plus'; 7 | } 8 | -------------------------------------------------------------------------------- /t/01-plus.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Test::Most; 3 | use Net::Ping; 4 | 5 | BEGIN { 6 | plan skip_all => 'these tests are not for smoke testing' 7 | if $ENV{AUTOMATED_TESTING}; 8 | plan skip_all => 'Needs Google+ API key in %ENV' 9 | unless $ENV{GOOGLE_PLUS_API_KEY}; 10 | 11 | my $p = Net::Ping->new( 'syn', 2 ); 12 | $p->port_number( scalar getservbyname( https => 'tcp' ) ); 13 | $p->service_check(1); 14 | plan skip_all => 'Needs access to remote Google API endpoint' 15 | unless $p->ping('www.googleapis.com'); 16 | 17 | plan tests => 4; 18 | } 19 | 20 | use Google::Plus; 21 | 22 | # Google+Zak 23 | my $user_id = '112708775709583792684'; 24 | 25 | my $g; 26 | subtest 'create object' => sub { 27 | plan tests => 5; 28 | 29 | can_ok 'Google::Plus' => 'new'; 30 | throws_ok { Google::Plus->new } qr/key.+required/, 'needs API key'; 31 | throws_ok { Google::Plus->new( blah => 'ther' ) } qr/key.+required/, 32 | 'bad key'; 33 | 34 | $g = new_ok 'Google::Plus', [ key => $ENV{GOOGLE_PLUS_API_KEY} ]; 35 | ok ref $g => 'Made object'; 36 | }; 37 | 38 | my $person; 39 | subtest 'get person profile' => sub { 40 | plan tests => 7; 41 | 42 | can_ok $g => 'person'; 43 | 44 | throws_ok { $g->person } qr/ID.+required/, 'needs user ID'; 45 | throws_ok { $g->person('foo') } qr/Invalid/, 'user ID must be numeric'; 46 | throws_ok { $g->person('00000000000000000000') } qr/Not Found/, 47 | 'person not found'; 48 | 49 | $person = $g->person($user_id); 50 | isa_ok $person => 'HASH'; 51 | 52 | subtest 'person properties' => sub { 53 | plan tests => 10; 54 | 55 | ok $person->{$_}, "$_ exists" 56 | for qw/ aboutMe displayName gender id image organizations placesLived 57 | tagline url urls /; 58 | }; 59 | 60 | subtest 'person profile partial response' => sub { 61 | plan tests => 6; 62 | 63 | my @fields = qw(displayName gender aboutMe); 64 | 65 | my $partial = $g->person( $user_id, join ',' => @fields ); 66 | isa_ok $partial => 'HASH', 'got partial response for person'; 67 | 68 | ok $partial->{$_}, "$_ exists in partial response" for @fields; 69 | 70 | ok !exists $partial->{birthday}, "birthday should not be in response"; 71 | 72 | throws_ok { $g->person( $user_id, 'invalid,fields' ) } qr/Invalid field/, 73 | "partial response using invalid field names"; 74 | }; 75 | }; 76 | 77 | my $activities; 78 | subtest 'get person activities' => sub { 79 | plan tests => 8; 80 | 81 | can_ok $g => 'activities'; 82 | 83 | throws_ok { $g->activities } qr/ID.+required/, 'needs user ID'; 84 | throws_ok { $g->activities('foo') } qr/Invalid/, 'user ID must be numeric'; 85 | throws_ok { $g->activities('00000000000000000000') } qr/Not Found/, 86 | 'person not found (to get activity list from)'; 87 | 88 | subtest 'get person activities per collection' => sub { 89 | plan tests => 3; 90 | 91 | $activities = $g->activities($user_id); 92 | isa_ok $activities => 'HASH', 'get activities (default public)'; 93 | 94 | $activities = $g->activities( $user_id => 'public' ); 95 | isa_ok $activities => 'HASH', 'get activities (explicit public)'; 96 | 97 | SKIP: { 98 | skip 'no custom collections for activities/list yet', 1; 99 | $activities = $g->activities( $user_id => 'cats' ); 100 | isa_ok $activities => 'HASH', 'get activities (custom collection)'; 101 | } 102 | }; 103 | 104 | subtest 'activity list properties' => sub { 105 | plan tests => 4; 106 | 107 | ok $activities->{$_}, "$_ in activity list exists" 108 | for qw/ items nextPageToken title updated /; 109 | }; 110 | 111 | subtest 'next activity list' => sub { 112 | plan tests => 5; 113 | 114 | my $next = 115 | $g->activities( $user_id => 'public', $activities->{nextPageToken} ); 116 | isnt $next->{nextPageToken}, $activities->{nextPageToken}, 117 | 'got new activity list (the next page)'; 118 | ok $next->{$_}, "$_ in next activity list exists" 119 | for qw/ items nextPageToken title updated /; 120 | }; 121 | 122 | subtest 'activity list partial response' => sub { 123 | plan tests => 5; 124 | 125 | my @fields = qw(title updated); 126 | 127 | my $partial = $g->activities( $user_id, undef, undef, join ',' => @fields ); 128 | isa_ok $partial => 'HASH', 'got partial response for activity list'; 129 | 130 | ok $partial->{$_}, "$_ exists in partial response" for @fields; 131 | 132 | ok !exists $partial->{items}, "items should not be in response"; 133 | 134 | throws_ok { $g->activities( $user_id, undef, undef, 'invalid,fields' ) } 135 | qr/Invalid field/, "partial response using invalid field names/strings"; 136 | }; 137 | }; 138 | 139 | # Google+ is the _vehicle_, Google Hangouts is the _product_. 140 | my $post = 'z13uxtsawqqwwbcjt04cdhsxcnfyir44xeg'; 141 | 142 | my $activity; 143 | subtest 'get activity' => sub { 144 | plan tests => 7; 145 | 146 | can_ok $g => 'activity'; 147 | 148 | throws_ok { $g->activity } qr/ID.+required/, 'needs activity ID'; 149 | throws_ok { $g->activity('$!@$@#$') } qr/Invalid activity/, 'bad activity'; 150 | throws_ok { $g->activity('foobarbaz') } qr/Not Found/, 151 | 'no activity named "foobarbaz"'; 152 | 153 | $activity = $g->activity($post); 154 | isa_ok $activity => 'HASH'; 155 | 156 | subtest 'activity properties' => sub { 157 | plan tests => 10; 158 | 159 | ok $activity->{$_}, "activity property $_ exists" 160 | for qw/ access actor annotation id object published title updated 161 | url verb /; 162 | }; 163 | 164 | subtest 'activity detail partial response' => sub { 165 | plan tests => 7; 166 | 167 | my @fields = qw(id title object url); 168 | 169 | my $partial = $g->activity( $post, join ',' => @fields ); 170 | isa_ok $partial => 'HASH', 'got partial response for activity'; 171 | 172 | ok $partial->{$_}, "$_ exists in partial response" for @fields; 173 | 174 | ok !exists $partial->{updated}, 175 | 'updated property should not be in response'; 176 | 177 | # Google throws a 404 here unlike when requesting other partials 178 | throws_ok { $g->activities( $post, 'invalid,fields' ) } qr/Invalid string/, 179 | "partial response using invalid field names/strings"; 180 | }; 181 | }; 182 | --------------------------------------------------------------------------------