File Coverage

blib/lib/Facebook/OpenGraph.pm
Criterion Covered Total %
statement 252 259 97.3
branch 83 104 79.8
condition 46 60 76.6
subroutine 58 58 100.0
pod 42 43 97.6
total 481 524 91.7


line stmt bran cond sub pod time code
1             package Facebook::OpenGraph;
2 33     33   2368919 use strict;
  33         203  
  33         813  
3 33     33   145 use warnings;
  33         53  
  33         723  
4 33     33   745 use 5.008001;
  33         103  
5              
6 33     33   11685 use Facebook::OpenGraph::Response;
  33         84  
  33         923  
7 33     33   14647 use HTTP::Request::Common;
  33         651041  
  33         2363  
8 33     33   240 use URI;
  33         67  
  33         709  
9 33     33   5903 use Furl::HTTP;
  33         166662  
  33         1079  
10 33     33   16897 use Data::Recursive::Encode;
  33         344369  
  33         1172  
11 33     33   236 use JSON 2 ();
  33         696  
  33         641  
12 33     33   152 use Carp qw(croak);
  33         57  
  33         2944  
13 33     33   14558 use Digest::SHA qw(hmac_sha256 hmac_sha256_hex);
  33         83493  
  33         2380  
14 33     33   12556 use MIME::Base64::URLSafe qw(urlsafe_b64decode);
  33         39768  
  33         1752  
15 33     33   204 use Scalar::Util qw(blessed);
  33         61  
  33         94615  
16              
17             our $VERSION = '1.31';
18              
19             sub new {
20 67     67 1 149432 my $class = shift;
21 67   100     292 my $args = shift || +{};
22              
23             return bless +{
24             app_id => $args->{app_id},
25             secret => $args->{secret},
26             namespace => $args->{namespace},
27             access_token => $args->{access_token},
28             redirect_uri => $args->{redirect_uri},
29             batch_limit => $args->{batch_limit} || 50,
30             is_beta => $args->{is_beta} || 0,
31             json => $args->{json} || JSON->new->utf8,
32             use_appsecret_proof => $args->{use_appsecret_proof} || 0,
33             use_post_method => $args->{use_post_method} || 0,
34             version => $args->{version} || undef,
35 67   100     2323 ua => $args->{ua} || Furl::HTTP->new(
      100        
      33        
      100        
      100        
      100        
      66        
36             capture_request => 1,
37             agent => __PACKAGE__ . '/' . $VERSION,
38             ),
39             }, $class;
40             }
41              
42             # accessors
43 33     33 1 208 sub app_id { shift->{app_id} }
44 29     29 1 243 sub secret { shift->{secret} }
45 43     43 1 698 sub ua { shift->{ua} }
46 4     4 1 27 sub namespace { shift->{namespace} }
47 88     88 1 2766 sub access_token { shift->{access_token} }
48 16     16 1 67 sub redirect_uri { shift->{redirect_uri} }
49 17     17 1 287 sub batch_limit { shift->{batch_limit} }
50 62     62 1 218 sub is_beta { shift->{is_beta} }
51 69     69 1 1170 sub json { shift->{json} }
52 43     43 1 125 sub use_appsecret_proof { shift->{use_appsecret_proof} }
53 2     2 1 8 sub use_post_method { shift->{use_post_method} }
54 64     64 1 243 sub version { shift->{version} }
55              
56             sub uri {
57 47     47 1 12552 my $self = shift;
58              
59 47 100       120 my $base = $self->is_beta ? 'https://graph.beta.facebook.com/'
60             : 'https://graph.facebook.com/'
61             ;
62              
63 47         176 return $self->_uri($base, @_);
64             }
65              
66             sub video_uri {
67 9     9 1 12619 my $self = shift;
68              
69 9 100       23 my $base = $self->is_beta ? 'https://graph-video.beta.facebook.com/'
70             : 'https://graph-video.facebook.com/'
71             ;
72              
73 9         30 return $self->_uri($base, @_);
74             }
75              
76             sub site_uri {
77 4     4 1 15 my $self = shift;
78              
79 4 100       19 my $base = $self->is_beta ? 'https://www.beta.facebook.com/'
80             : 'https://www.facebook.com/'
81             ;
82              
83 4         14 return $self->_uri($base, @_);
84             }
85              
86             sub _uri {
87 60     60   155 my ($self, $base, $path, $param_ref) = @_;
88 60   100     448 my $uri = URI->new_abs($path || '/', $base);
89             $uri->query_form(+{
90             $uri->query_form, # when given $path is like /foo?bar=bazz
91 60 100       178925 %{ $param_ref || +{} }, # additional query parameter
  60         1261  
92             });
93              
94 60         3377 return $uri;
95             }
96              
97             # Login for Games on Facebook > Checking Login Status > Parsing the Signed Request
98             # https://developers.facebook.com/docs/facebook-login/using-login-with-games
99             sub parse_signed_request {
100 5     5 1 124 my ($self, $signed_request) = @_;
101 5 50       11 croak 'signed_request is not given' unless $signed_request;
102 5 100       13 croak 'secret key must be set' unless $self->secret;
103              
104             # "1. Split the signed request into two parts delineated by a '.' character
105             # (eg. 238fsdfsd.oijdoifjsidf899)"
106 4         18 my ($enc_sig, $payload) = split(m{ \. }xms, $signed_request);
107              
108             # "2. Decode the first part - the encoded signature - from base64url"
109 4         14 my $sig = urlsafe_b64decode($enc_sig);
110              
111             # "3. Decode the second part - the 'payload' - from base64url and then
112             # decode the resultant JSON object"
113 4         63 my $val = $self->json->decode(urlsafe_b64decode($payload));
114              
115             # "It specifically uses HMAC-SHA256 encoding, which you can again use with
116             # most programming languages."
117             croak 'algorithm must be HMAC-SHA256'
118 4 50       84 unless uc( $val->{algorithm} ) eq 'HMAC-SHA256';
119              
120             # "You can compare this encoded signature with an expected signature using
121             # the payload you received as well as the app secret which is known only to
122             # your and ensure that they match."
123 4         10 my $expected_sig = hmac_sha256($payload, $self->secret);
124 4 50       13 croak 'Signature does not match' unless $sig eq $expected_sig;
125              
126 4         9 return $val;
127             }
128              
129             # Detailed flow is described here.
130             # Manually Build a Login Flow > Logging people in > Invoking the login dialog
131             # https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/
132             #
133             # Parameters for login dialog are shown here.
134             # Login Dialog > Parameters
135             # https://developers.facebook.com/docs/reference/dialogs/oauth/
136             sub auth_uri {
137 6     6 1 467 my ($self, $param_ref) = @_;
138 6   100     17 $param_ref ||= +{};
139 6 100 66     19 croak 'redirect_uri and app_id must be set'
140             unless $self->redirect_uri && $self->app_id;
141              
142             # "A comma separated list of permission names which you would like people
143             # to grant your app."
144 5 100       17 if (my $scope_ref = ref $param_ref->{scope}) {
145 4 100       21 croak 'scope must be string or array ref' unless $scope_ref eq 'ARRAY';
146 3         6 $param_ref->{scope} = join q{,}, @{ $param_ref->{scope} };
  3         13  
147             }
148              
149             # "The URL to redirect to after a button is clicked or tapped in the
150             # dialog."
151 4         6 $param_ref->{redirect_uri} = $self->redirect_uri;
152              
153             # "Your App ID. This is called client_id instead of app_id for this
154             # particular method in order to be compliant with the OAuth 2.0
155             # specification."
156 4         8 $param_ref->{client_id} = $self->app_id;
157              
158             # "If you are using the URL redirect dialog implementation, then this will
159             # be a full page display, shown within Facebook.com. This display type is
160             # called page."
161 4   50     18 $param_ref->{display} ||= 'page';
162              
163             # "Response data is included as URL parameters and contains code parameter
164             # (an encrypted string unique to each login request). This is the default
165             # behaviour if this parameter is not specified."
166 4   50     15 $param_ref->{response_type} ||= 'code';
167              
168 4         12 my $uri = $self->site_uri('/dialog/oauth', $param_ref);
169              
170             # Platform Versioning > Making Versioned Requests > Dialogs.
171             # https://developers.facebook.com/docs/apps/versions#dialogs
172 4         12 $uri->path( $self->gen_versioned_path($uri->path) );
173              
174 4         127 return $uri->as_string;
175             }
176              
177             sub set_access_token {
178 4     4 1 13 my ($self, $token) = @_;
179 4         10 $self->{access_token} = $token;
180             }
181              
182             # Access Tokens > App Tokens
183             # https://developers.facebook.com/docs/facebook-login/access-tokens/#apptokens
184             sub get_app_token {
185 3     3 1 138 my $self = shift;
186              
187             # Document does not mention what grant_type is all about or what values can
188             # be set, but RFC 6749 covers the basic idea of grant types and its Section
189             # 4.4 describes Client Credentials Grant.
190             # http://tools.ietf.org/html/rfc6749#section-4.4
191 3         14 return $self->_get_token(+{grant_type => 'client_credentials'});
192             }
193              
194             # Manually Build a Login Flow > Confirming identity > Exchanging code for an access token
195             # https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow
196             sub get_user_token_by_code {
197 2     2 1 43 my ($self, $code) = @_;
198              
199 2 50       6 croak 'code is not given' unless $code;
200 2 50       8 croak 'redirect_uri must be set' unless $self->redirect_uri;
201              
202 2         7 my $query_ref = +{
203             redirect_uri => $self->redirect_uri,
204             code => $code,
205             };
206 2         7 return $self->_get_token($query_ref);
207             }
208              
209             sub get_user_token_by_cookie {
210 4     4 1 197 my ($self, $cookie_value) = @_;
211              
212 4 100       26 croak 'cookie value is not given' unless $cookie_value;
213              
214 3         9 my $parsed_signed_request = $self->parse_signed_request($cookie_value);
215              
216             # https://github.com/oklahomer/p5-Facebook-OpenGraph/issues/1#issuecomment-41065480
217             # parsed content should be something like below.
218             # {
219             # algorithm => "HMAC-SHA256",
220             # issued_at => 1398180151,
221             # code => "SOME_OPAQUE_STRING",
222             # user_id => 44007581,
223             # };
224             croak q{"code" is not contained in cookie value: } . $cookie_value
225 3 100       16 unless $parsed_signed_request->{code};
226              
227             # Redirect_uri MUST be empty string in this case.
228             # That's why I didn't use get_user_token_by_code().
229             my $query_ref = +{
230             code => $parsed_signed_request->{code},
231 2         5 redirect_uri => '',
232             };
233 2         7 return $self->_get_token($query_ref);
234             }
235              
236             # Access Tokens > Expiration and Extending Tokens
237             # https://developers.facebook.com/docs/facebook-login/access-tokens/
238             sub exchange_token {
239 3     3 1 124 my ($self, $short_term_token) = @_;
240              
241 3 50       8 croak 'short term token is not given' unless $short_term_token;
242              
243 3         10 my $query_ref = +{
244             grant_type => 'fb_exchange_token',
245             fb_exchange_token => $short_term_token,
246             };
247 3         8 return $self->_get_token($query_ref);
248             }
249              
250             sub _get_token {
251 10     10   20 my ($self, $param_ref) = @_;
252              
253 10 100 100     26 croak 'app_id and secret must be set' unless $self->app_id && $self->secret;
254              
255 6         22 $param_ref = +{
256             %$param_ref,
257             client_id => $self->app_id,
258             client_secret => $self->secret,
259             };
260              
261 6         26 my $response = $self->request('GET', '/oauth/access_token', $param_ref);
262             # Document describes as follows:
263             # "The response you will receive from this endpoint, if successful, is
264             # access_token={access-token}&expires={seconds-til-expiration}
265             # If it is not successful, you'll receive an explanatory error message."
266             #
267             # It, however, returnes no "expires" parameter on some edge cases.
268             # e.g. Your app requests manage_pages permission.
269             # https://developers.facebook.com/bugs/597779113651383/
270 6 100       25 if ($response->is_api_version_eq_or_later_than('v2.3')) {
271             # As of v2.3, to be compliant with RFC 6749, response is JSON formatted
272             # as described below.
273             # {"access_token": , "token_type":, "expires_in":
274             # https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.3#confirm
275 1         5 return $response->as_hashref;
276             }
277              
278 5         22 my $res_content = $response->content;
279 5         28 my $token_ref = +{ URI->new("?$res_content")->query_form };
280             croak "can't get access_token properly: $res_content"
281 5 50       673 unless $token_ref->{access_token};
282              
283 5         63 return $token_ref;
284             }
285              
286             sub get {
287 10     10 1 265 return shift->request('GET', @_)->as_hashref;
288             }
289              
290             sub post {
291 22     22 1 183 return shift->request('POST', @_)->as_hashref;
292             }
293              
294             # Deleting > Objects
295             # https://developers.facebook.com/docs/reference/api/deleting/
296             sub delete {
297 1     1 1 15 return shift->request('DELETE', @_)->as_hashref;
298             }
299              
300             # For those who got used to Facebook::Graph
301             *fetch = \&get;
302             *publish = \&post;
303              
304             # Using ETags
305             # https://developers.facebook.com/docs/reference/ads-api/etags-reference/
306             sub fetch_with_etag {
307 2     2 1 41 my ($self, $uri, $param_ref, $etag) = @_;
308              
309             # Attach ETag value to header
310             # Returns status 304 without contnet, or status 200 with modified content
311 2         4 my $header = ['IF-None-Match' => $etag];
312 2         7 my $response = $self->request('GET', $uri, $param_ref, $header);
313              
314 2 100       16 return $response->is_modified ? $response->as_hashref : undef;
315             }
316              
317             sub bulk_fetch {
318 1     1 1 23 my ($self, $paths_ref) = @_;
319              
320             my @queries = map {
321 1         3 +{
322 2         8 method => 'GET',
323             relative_url => $_,
324             }
325             } @$paths_ref;
326              
327 1         5 return $self->batch(\@queries);
328             }
329              
330             # Making Multiple API Requests > Making a simple batched request
331             # https://developers.facebook.com/docs/graph-api/making-multiple-requests
332             sub batch {
333 6     6 1 158 my $self = shift;
334              
335 6         19 my $responses_ref = $self->batch_fast(@_);
336              
337             # Devide response content and create response objects that correspond to
338             # each request
339 5         8 my @data = ();
340 5         14 for my $r (@$responses_ref) {
341 7         11 for my $res_ref (@$r) {
342             my $response = $self->create_response(
343             $res_ref->{code},
344             $res_ref->{message},
345 58         100 [ map { $_->{name} => $_->{value} } @{ $res_ref->{headers} } ],
  13         27  
346             $res_ref->{body},
347 13         31 );
348 13 50       46 croak $response->error_string unless $response->is_success;
349              
350 13         52 push @data, $response->as_hashref;
351             }
352             }
353              
354 5         34 return \@data;
355             }
356              
357             # doesn't create F::OG::Response object for each response
358             sub batch_fast {
359 6     6 1 8 my $self = shift;
360 6         11 my $batch = shift;
361              
362             # Other than HTTP header, you need to set access_token as top level
363             # parameter. You can specify individual token for each request so you can
364             # act as several other users and pages.
365 6 100       15 croak 'Top level access_token must be set' unless $self->access_token;
366              
367             # "We currently limit the number of requests which can be in a batch to 50"
368 5         12 my @responses = ();
369 5         17 while(my @queries = splice @$batch, 0, $self->batch_limit) {
370              
371 7         14 for my $q (@queries) {
372 13 50 66     48 if ($q->{method} eq 'POST' && $q->{body}) {
373 3         8 my $body_ref = $self->prep_param($q->{body});
374 3         14 my $uri = URI->new;
375 3         3586 $uri->query_form(%$body_ref);
376 3         380 $q->{body} = $uri->query;
377             }
378             }
379              
380 7         44 my @req = (
381             '/',
382             +{
383             access_token => $self->access_token,
384             batch => $self->json->encode(\@queries),
385             },
386             @_,
387             );
388              
389 7         24 push @responses, $self->post(@req);
390             }
391              
392 5         13 return \@responses;
393             }
394              
395             # Facebook Query Language (FQL) Overview
396             # https://developers.facebook.com/docs/technical-guides/fql/
397             sub fql {
398 2     2 1 22 my $self = shift;
399 2         4 my $query = shift;
400 2         10 return $self->get('/fql', +{q => $query}, @_);
401             }
402              
403             # Facebook Query Language (FQL) Overview: Multi-query
404             # https://developers.facebook.com/docs/technical-guides/fql/#multi
405             sub bulk_fql {
406 1     1 1 21 my $self = shift;
407 1         2 my $batch = shift;
408 1         4 return $self->fql($self->json->encode($batch), @_);
409             }
410              
411             sub request {
412 41     41 1 132 my ($self, $method, $uri, $param_ref, $headers) = @_;
413              
414 41         100 $method = uc $method;
415 41 50 33     265 $uri = $self->uri($uri) unless blessed($uri) && $uri->isa('URI');
416 41         131 $uri->path( $self->gen_versioned_path($uri->path) );
417             $param_ref = $self->prep_param(+{
418             $uri->query_form(+{}),
419 41 100       1279 %{$param_ref || +{}},
  41         1609  
420             });
421              
422             # Securing Graph API Requests > Verifying Graph API Calls with appsecret_proof
423             # https://developers.facebook.com/docs/graph-api/securing-requests/
424 41 100       162 if ($self->use_appsecret_proof) {
425 1         3 $param_ref->{appsecret_proof} = $self->gen_appsecret_proof;
426             }
427              
428             # Use POST as default HTTP method and add method=(POST|GET|DELETE) to query
429             # parameter. Document only says we can send HTTP DELETE method or, instead,
430             # HTTP POST method with ?method=delete to delete object. It does not say
431             # POST with method=(get|post) parameter works, but PHP SDK always sends POST
432             # with method parameter so... I just give you this option.
433             # Check PHP SDK's base_facebook.php for detail.
434 41 100       123 if ($self->{use_post_method}) {
435 2         4 $param_ref->{method} = $method;
436 2         3 $method = 'POST';
437             }
438              
439 41   100     184 $headers ||= [];
440              
441             # Document says we can pass access_token as a part of query parameter,
442             # but it actually supports Authorization header to be compliant with the
443             # OAuth 2.0 spec.
444             # http://tools.ietf.org/html/rfc6749#section-7
445 41 100       109 if ($self->access_token) {
446 20         54 push @$headers, (
447             'Authorization',
448             sprintf('OAuth %s', $self->access_token),
449             );
450             }
451              
452 41         81 my $content = q{};
453 41 100       113 if ($method eq 'POST') {
454 24 100 100     216 if ($param_ref->{source}
      100        
      100        
455             || $param_ref->{file}
456             || $param_ref->{upload_phase}
457             || $param_ref->{captions_file}) {
458             # post image, video or caption file
459              
460             # https://developers.facebook.com/docs/reference/api/video/
461             # When posting a video, use graph-video.facebook.com .
462             # base_facebook.php has an equivalent part in isVideoPost().
463             # ($method == 'POST' && preg_match("/^(\/)(.+)(\/)(videos)$/", $path))
464             # For other actions, use graph.facebook.com/VIDEO_ID/CONNECTION_TYPE
465 6 100       17 if ($uri->path =~ m{\A /.+/videos \z}xms) {
466 3         43 $uri->host($self->video_uri->host);
467             }
468              
469             # Content-Type should be multipart/form-data
470             # https://developers.facebook.com/docs/reference/api/publishing/
471 6         299 push @$headers, (Content_Type => 'form-data');
472              
473             # Furl::HTTP document says we can use multipart/form-data with
474             # HTTP::Request::Common.
475 6         40 my $req = POST $uri, @$headers, Content => [%$param_ref];
476 6         53549 $content = $req->content;
477 6         74 my $req_header = $req->headers;
478             $headers = +[
479             map {
480 6         48 my $k = $_;
  18         160  
481 18         35 map { ( $k => $_ ) } $req_header->header($k);
  18         496  
482             } $req_header->header_field_names
483             ];
484             }
485             else {
486             # Post simple parameters such as message, link, description, etc...
487             # Content-Type: application/x-www-form-urlencoded will be set in
488             # Furl::HTTP, and $content will be treated properly.
489 18         34 $content = $param_ref;
490             }
491             }
492             else {
493 17         53 $uri->query_form($param_ref);
494             }
495              
496 41         1594 my ($res_minor_version, @res_elms) = $self->ua->request(
497             method => $method,
498             url => $uri,
499             headers => $headers,
500             content => $content,
501             );
502              
503 41         75706 my $res = $self->create_response(@res_elms);
504              
505             # return F::OG::Response object on success
506 41 100       178 return $res if $res->is_success;
507              
508             # Use later version of Furl::HTTP to utilize req_headers and req_content.
509             # This Should be helpful when debugging.
510 4         15 my $msg = $res->error_string;
511 4 50       16 if ($res->req_headers) {
512 0         0 $msg .= "\n" . $res->req_headers . $res->req_content;
513             }
514 4         65 croak $msg;
515             }
516              
517             # Securing Graph API Requests > Verifying Graph API Calls with appsecret_proof > Generating the proof
518             # https://developers.facebook.com/docs/graph-api/securing-requests/
519             sub gen_appsecret_proof {
520 2     2 1 51 my $self = shift;
521 2 50       7 croak 'app secret must be set' unless $self->secret;
522 2 50       8 croak 'access_token must be set' unless $self->access_token;
523 2         5 return hmac_sha256_hex($self->access_token, $self->secret);
524             }
525              
526             # Platform Versioning > Making Versioned Requests
527             # https://developers.facebook.com/docs/apps/versions
528             sub gen_versioned_path {
529 57     57 0 585 my ($self, $path) = @_;
530              
531 57 100       139 $path = '/' unless $path;
532              
533 57 100 100     149 if ($self->version && $path !~ m{\A /v(?:\d+)\.(?:\d+)/ }x) {
534             # If default platform version is set on initialisation
535             # and given path doesn't contain version,
536             # then prepend the default version.
537 5         10 $path = sprintf('/%s%s', $self->version, $path);
538             }
539              
540 57         184 return $path;
541             }
542              
543             sub js_cookie_name {
544 2     2 1 193 my $self = shift;
545 2 100       6 croak 'app_id must be set' unless $self->app_id;
546              
547             # Cookie is set by JS SDK with a name of fbsr_{app_id}. Official document
548             # is not provided for more than 3 yaers so I quote from PHP SDK's code.
549             # "Constructs and returns the name of the cookie that potentially houses
550             # the signed request for the app user. The cookie is not set by the
551             # BaseFacebook class, but it may be set by the JavaScript SDK."
552             # The cookie value can be parsed as signed request and it contains 'code'
553             # to exchange for access toekn.
554 1         4 return sprintf('fbsr_%d', $self->app_id);
555             }
556              
557             sub create_response {
558 54     54 1 132 my $self = shift;
559             return Facebook::OpenGraph::Response->new(+{
560             json => $self->json,
561             map {
562 54         185 $_ => shift
  324         813  
563             } qw/code message headers content req_headers req_content/,
564             });
565             }
566              
567             sub prep_param {
568 45     45 1 158 my ($self, $param_ref) = @_;
569              
570 45   50     339 $param_ref = Data::Recursive::Encode->encode_utf8($param_ref || +{});
571              
572             # /?ids=4,http://facebook-docs.oklahome.net
573 45 100       3517 if (my $ids = $param_ref->{ids}) {
574 1 50       6 $param_ref->{ids} = ref $ids ? join q{,}, @$ids : $ids;
575             }
576              
577             # mostly for /APP_ID/accounts/test-users
578 45 100       128 if (my $perms = $param_ref->{permissions}) {
579 5 100       22 $param_ref->{permissions} = ref $perms ? join q{,}, @$perms : $perms;
580             }
581              
582             # Source, file, video_file_chunk and captions_file parameter contains file path.
583             # It must be an array ref to work with HTTP::Request::Common.
584 45         106 for my $file (qw/source file video_file_chunk captions_file/) {
585 180 100       373 next unless my $path = $param_ref->{$file};
586 6 50       26 $param_ref->{$file} = ref $path ? $path : [$path];
587             }
588              
589             # use Field Expansion
590 45 100       159 if (my $field_ref = $param_ref->{fields}) {
591 2         8 $param_ref->{fields} = $self->prep_fields_recursive($field_ref);
592             }
593              
594             # Using Objects: Using the Object API
595             # https://developers.facebook.com/docs/opengraph/using-objects/#objectapi
596 45         86 my $object = $param_ref->{object};
597 45 100 66     155 if ($object && ref $object eq 'HASH') {
598 1         3 $param_ref->{object} = $self->json->encode($object);
599             }
600              
601 45         88 return $param_ref;
602             }
603              
604             # Using the Graph API: Reading > Choosing Fields > Making Nested Requests
605             # https://developers.facebook.com/docs/graph-api/using-graph-api/
606             sub prep_fields_recursive {
607 4     4 1 9 my ($self, $val) = @_;
608              
609 4         8 my $ref = ref $val;
610 4 100       10 if (!$ref) {
    50          
    0          
611 3         10 return $val;
612             }
613             elsif ($ref eq 'ARRAY') {
614 1         2 return join q{,}, map { $self->prep_fields_recursive($_) } @$val;
  2         15  
615             }
616             elsif ($ref eq 'HASH') {
617 0         0 my @strs = ();
618 0         0 while (my ($k, $v) = each %$val) {
619 0         0 my $r = ref $v;
620 0 0 0     0 my $pattern = $r && $r eq 'HASH' ? '%s.%s' : '%s(%s)';
621 0         0 push @strs, sprintf($pattern, $k, $self->prep_fields_recursive($v));
622             }
623 0         0 return join q{.}, @strs;
624             }
625             }
626              
627             # Using Actions > Publishing Actions
628             # https://developers.facebook.com/docs/opengraph/using-actions/#publish
629             sub publish_action {
630 1     1 1 22 my $self = shift;
631 1         3 my $action = shift;
632 1 50       3 croak 'namespace is not set' unless $self->namespace;
633 1         4 return $self->post(sprintf('/me/%s:%s', $self->namespace, $action), @_);
634             }
635              
636             # Using Objects > Using the Object API > Images with the Object API
637             # https://developers.facebook.com/docs/opengraph/using-objects/
638             sub publish_staging_resource {
639 1     1 1 24 my $self = shift;
640 1         2 my $file = shift;
641 1         5 return $self->post('/me/staging_resources', +{file => $file}, @_);
642             }
643              
644             # Test Users: Creating
645             # https://developers.facebook.com/docs/test_users/
646             sub create_test_users {
647 2     2 1 43 my $self = shift;
648 2         3 my $settings_ref = shift;
649              
650 2 100       8 if (ref $settings_ref ne 'ARRAY') {
651 1         2 $settings_ref = [$settings_ref];
652             }
653              
654 2         12 my @settings = ();
655 2         6 my $relative_url = sprintf('/%s/accounts/test-users', $self->app_id);
656 2         6 for my $setting (@$settings_ref) {
657 3         12 push @settings, +{
658             method => 'POST',
659             relative_url => $relative_url,
660             body => $setting,
661             };
662             }
663              
664 2         6 return $self->batch(\@settings);
665             }
666              
667             # Using Objects > Using Self-Hosted Objects > Updating Objects
668             # https://developers.facebook.com/docs/opengraph/using-objects/
669             sub check_object {
670 6     6 1 208 my ($self, $target) = @_;
671 6         15 my $param_ref = +{
672             id => $target, # $target is object url or open graph object id
673             scrape => 'true',
674             };
675 6         13 return $self->post(q{}, $param_ref);
676             }
677              
678             1;
679             __END__