File Coverage

blib/lib/Facebook/OpenGraph.pm
Criterion Covered Total %
statement 255 262 97.3
branch 89 112 79.4
condition 43 57 75.4
subroutine 58 58 100.0
pod 42 43 97.6
total 487 532 91.5


line stmt bran cond sub pod time code
1             package Facebook::OpenGraph;
2 33     33   2701565 use strict;
  33         234  
  33         949  
3 33     33   184 use warnings;
  33         63  
  33         789  
4 33     33   737 use 5.008001;
  33         115  
5              
6 33     33   13631 use Facebook::OpenGraph::Response;
  33         89  
  33         1073  
7 33     33   15920 use HTTP::Request::Common;
  33         756530  
  33         2428  
8 33     33   265 use URI;
  33         75  
  33         775  
9 33     33   6474 use Furl::HTTP;
  33         194080  
  33         1154  
10 33     33   15317 use Data::Recursive::Encode;
  33         392766  
  33         1304  
11 33     33   255 use JSON 2 ();
  33         769  
  33         773  
12 33     33   177 use Carp qw(croak);
  33         73  
  33         1667  
13 33     33   16765 use Digest::SHA qw(hmac_sha256 hmac_sha256_hex);
  33         97100  
  33         2849  
14 33     33   14787 use MIME::Base64::URLSafe qw(urlsafe_b64decode);
  33         44215  
  33         2004  
15 33     33   238 use Scalar::Util qw(blessed);
  33         113  
  33         111595  
16              
17             our $VERSION = '1.30';
18              
19             sub new {
20 66     66 1 151669 my $class = shift;
21 66   100     310 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 66   100     2362 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 242 sub app_id { shift->{app_id} }
44 29     29 1 253 sub secret { shift->{secret} }
45 42     42 1 801 sub ua { shift->{ua} }
46 4     4 1 23 sub namespace { shift->{namespace} }
47 86     86 1 2861 sub access_token { shift->{access_token} }
48 16     16 1 89 sub redirect_uri { shift->{redirect_uri} }
49 17     17 1 337 sub batch_limit { shift->{batch_limit} }
50 61     61 1 240 sub is_beta { shift->{is_beta} }
51 68     68 1 1312 sub json { shift->{json} }
52 42     42 1 147 sub use_appsecret_proof { shift->{use_appsecret_proof} }
53 2     2 1 10 sub use_post_method { shift->{use_post_method} }
54 63     63 1 274 sub version { shift->{version} }
55              
56             sub uri {
57 46     46 1 15264 my $self = shift;
58              
59 46 100       133 my $base = $self->is_beta ? 'https://graph.beta.facebook.com/'
60             : 'https://graph.facebook.com/'
61             ;
62              
63 46         168 return $self->_uri($base, @_);
64             }
65              
66             sub video_uri {
67 9     9 1 15532 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         43 return $self->_uri($base, @_);
74             }
75              
76             sub site_uri {
77 4     4 1 7 my $self = shift;
78              
79 4 100       17 my $base = $self->is_beta ? 'https://www.beta.facebook.com/'
80             : 'https://www.facebook.com/'
81             ;
82              
83 4         15 return $self->_uri($base, @_);
84             }
85              
86             sub _uri {
87 59     59   170 my ($self, $base, $path, $param_ref) = @_;
88 59   100     381 my $uri = URI->new_abs($path || '/', $base);
89             $uri->query_form(+{
90             $uri->query_form, # when given $path is like /foo?bar=bazz
91 59 100       209418 %{ $param_ref || +{} }, # additional query parameter
  59         1403  
92             });
93              
94 59         3551 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 186 my ($self, $signed_request) = @_;
101 5 50       14 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         19 my ($enc_sig, $payload) = split(m{ \. }xms, $signed_request);
107              
108             # "2. Decode the first part - the encoded signature - from base64url"
109 4         18 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         73 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       86 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         11 my $expected_sig = hmac_sha256($payload, $self->secret);
124 4 50       24 croak 'Signature does not match' unless $sig eq $expected_sig;
125              
126 4         14 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 347 my ($self, $param_ref) = @_;
138 6   100     18 $param_ref ||= +{};
139 6 100 66     14 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       19 if (my $scope_ref = ref $param_ref->{scope}) {
145 4 100       20 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         10  
147             }
148              
149             # "The URL to redirect to after a button is clicked or tapped in the
150             # dialog."
151 4         9 $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         9 $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     16 $param_ref->{response_type} ||= 'code';
167              
168 4         10 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         13 $uri->path( $self->gen_versioned_path($uri->path) );
173              
174 4         143 return $uri->as_string;
175             }
176              
177             sub set_access_token {
178 4     4 1 15 my ($self, $token) = @_;
179 4         44 $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 149 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 45 my ($self, $code) = @_;
198              
199 2 50       6 croak 'code is not given' unless $code;
200 2 50       7 croak 'redirect_uri must be set' unless $self->redirect_uri;
201              
202 2         6 my $query_ref = +{
203             redirect_uri => $self->redirect_uri,
204             code => $code,
205             };
206 2         6 return $self->_get_token($query_ref);
207             }
208              
209             sub get_user_token_by_cookie {
210 4     4 1 223 my ($self, $cookie_value) = @_;
211              
212 4 100       28 croak 'cookie value is not given' unless $cookie_value;
213              
214 3         11 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       20 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         7 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 147 my ($self, $short_term_token) = @_;
240              
241 3 50       8 croak 'short term token is not given' unless $short_term_token;
242              
243 3         11 my $query_ref = +{
244             grant_type => 'fb_exchange_token',
245             fb_exchange_token => $short_term_token,
246             };
247 3         9 return $self->_get_token($query_ref);
248             }
249              
250             sub _get_token {
251 10     10   24 my ($self, $param_ref) = @_;
252              
253 10 100 100     24 croak 'app_id and secret must be set' unless $self->app_id && $self->secret;
254              
255 6         24 $param_ref = +{
256             %$param_ref,
257             client_id => $self->app_id,
258             client_secret => $self->secret,
259             };
260              
261 6         48 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       30 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         25 my $res_content = $response->content;
279 5         31 my $token_ref = +{ URI->new("?$res_content")->query_form };
280             croak "can't get access_token properly: $res_content"
281 5 50       835 unless $token_ref->{access_token};
282              
283 5         55 return $token_ref;
284             }
285              
286             sub get {
287 10     10 1 292 return shift->request('GET', @_)->as_hashref;
288             }
289              
290             sub post {
291 21     21 1 201 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 18 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 42 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         6 my $header = ['IF-None-Match' => $etag];
312 2         7 my $response = $self->request('GET', $uri, $param_ref, $header);
313              
314 2 100       7 return $response->is_modified ? $response->as_hashref : undef;
315             }
316              
317             sub bulk_fetch {
318 1     1 1 27 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 187 my $self = shift;
334              
335 6         18 my $responses_ref = $self->batch_fast(@_);
336              
337             # Devide response content and create response objects that correspond to
338             # each request
339 5         13 my @data = ();
340 5         13 for my $r (@$responses_ref) {
341 7         16 for my $res_ref (@$r) {
342             my $response = $self->create_response(
343             $res_ref->{code},
344             $res_ref->{message},
345 58         126 [ map { $_->{name} => $_->{value} } @{ $res_ref->{headers} } ],
  13         33  
346             $res_ref->{body},
347 13         38 );
348 13 50       100 croak $response->error_string unless $response->is_success;
349              
350 13         34 push @data, $response->as_hashref;
351             }
352             }
353              
354 5         61 return \@data;
355             }
356              
357             # doesn't create F::OG::Response object for each response
358             sub batch_fast {
359 6     6 1 12 my $self = shift;
360 6         9 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       20 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         21 my @responses = ();
369 5         20 while(my @queries = splice @$batch, 0, $self->batch_limit) {
370              
371 7         17 for my $q (@queries) {
372 13 50 66     57 if ($q->{method} eq 'POST' && $q->{body}) {
373 3         9 my $body_ref = $self->prep_param($q->{body});
374 3         14 my $uri = URI->new;
375 3         4589 $uri->query_form(%$body_ref);
376 3         399 $q->{body} = $uri->query;
377             }
378             }
379              
380 7         48 my @req = (
381             '/',
382             +{
383             access_token => $self->access_token,
384             batch => $self->json->encode(\@queries),
385             },
386             @_,
387             );
388              
389 7         28 push @responses, $self->post(@req);
390             }
391              
392 5         15 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 25 my $self = shift;
399 2         5 my $query = shift;
400 2         11 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 23 my $self = shift;
407 1         2 my $batch = shift;
408 1         3 return $self->fql($self->json->encode($batch), @_);
409             }
410              
411             sub request {
412 40     40 1 132 my ($self, $method, $uri, $param_ref, $headers) = @_;
413              
414 40         136 $method = uc $method;
415 40 50 33     280 $uri = $self->uri($uri) unless blessed($uri) && $uri->isa('URI');
416 40         151 $uri->path( $self->gen_versioned_path($uri->path) );
417             $param_ref = $self->prep_param(+{
418             $uri->query_form(+{}),
419 40 100       1547 %{$param_ref || +{}},
  40         1960  
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 40 100       193 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 40 100       124 if ($self->{use_post_method}) {
435 2         5 $param_ref->{method} = $method;
436 2         4 $method = 'POST';
437             }
438              
439 40   100     220 $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 40 100       127 if ($self->access_token) {
446 19         55 push @$headers, (
447             'Authorization',
448             sprintf('OAuth %s', $self->access_token),
449             );
450             }
451              
452 40         95 my $content = q{};
453 40 100       122 if ($method eq 'POST') {
454 23 100 100     152 if ($param_ref->{source} || $param_ref->{file} || $param_ref->{upload_phase} ) {
      100        
455             # post image or video file
456              
457             # https://developers.facebook.com/docs/reference/api/video/
458             # When posting a video, use graph-video.facebook.com .
459             # base_facebook.php has an equivalent part in isVideoPost().
460             # ($method == 'POST' && preg_match("/^(\/)(.+)(\/)(videos)$/", $path))
461             # For other actions, use graph.facebook.com/VIDEO_ID/CONNECTION_TYPE
462 5 100       15 if ($uri->path =~ m{\A /.+/videos \z}xms) {
463 3         47 $uri->host($self->video_uri->host);
464             }
465              
466             # Content-Type should be multipart/form-data
467             # https://developers.facebook.com/docs/reference/api/publishing/
468 5         328 push @$headers, (Content_Type => 'form-data');
469              
470             # Furl::HTTP document says we can use multipart/form-data with
471             # HTTP::Request::Common.
472 5         35 my $req = POST $uri, @$headers, Content => [%$param_ref];
473 5         63395 $content = $req->content;
474 5         133 my $req_header = $req->headers;
475             $headers = +[
476             map {
477 5         43 my $k = $_;
  15         153  
478 15         36 map { ( $k => $_ ) } $req_header->header($k);
  15         499  
479             } $req_header->header_field_names
480             ];
481             }
482             else {
483             # Post simple parameters such as message, link, description, etc...
484             # Content-Type: application/x-www-form-urlencoded will be set in
485             # Furl::HTTP, and $content will be treated properly.
486 18         36 $content = $param_ref;
487             }
488             }
489             else {
490 17         58 $uri->query_form($param_ref);
491             }
492              
493 40         1969 my ($res_minor_version, @res_elms) = $self->ua->request(
494             method => $method,
495             url => $uri,
496             headers => $headers,
497             content => $content,
498             );
499              
500 40         82992 my $res = $self->create_response(@res_elms);
501              
502             # return F::OG::Response object on success
503 40 100       209 return $res if $res->is_success;
504              
505             # Use later version of Furl::HTTP to utilize req_headers and req_content.
506             # This Should be helpful when debugging.
507 4         17 my $msg = $res->error_string;
508 4 50       15 if ($res->req_headers) {
509 0         0 $msg .= "\n" . $res->req_headers . $res->req_content;
510             }
511 4         73 croak $msg;
512             }
513              
514             # Securing Graph API Requests > Verifying Graph API Calls with appsecret_proof > Generating the proof
515             # https://developers.facebook.com/docs/graph-api/securing-requests/
516             sub gen_appsecret_proof {
517 2     2 1 62 my $self = shift;
518 2 50       8 croak 'app secret must be set' unless $self->secret;
519 2 50       7 croak 'access_token must be set' unless $self->access_token;
520 2         6 return hmac_sha256_hex($self->access_token, $self->secret);
521             }
522              
523             # Platform Versioning > Making Versioned Requests
524             # https://developers.facebook.com/docs/apps/versions
525             sub gen_versioned_path {
526 56     56 0 686 my ($self, $path) = @_;
527              
528 56 100       171 $path = '/' unless $path;
529              
530 56 100 100     166 if ($self->version && $path !~ m{\A /v(?:\d+)\.(?:\d+)/ }x) {
531             # If default platform version is set on initialisation
532             # and given path doesn't contain version,
533             # then prepend the default version.
534 5         12 $path = sprintf('/%s%s', $self->version, $path);
535             }
536              
537 56         228 return $path;
538             }
539              
540             sub js_cookie_name {
541 2     2 1 90 my $self = shift;
542 2 100       6 croak 'app_id must be set' unless $self->app_id;
543              
544             # Cookie is set by JS SDK with a name of fbsr_{app_id}. Official document
545             # is not provided for more than 3 yaers so I quote from PHP SDK's code.
546             # "Constructs and returns the name of the cookie that potentially houses
547             # the signed request for the app user. The cookie is not set by the
548             # BaseFacebook class, but it may be set by the JavaScript SDK."
549             # The cookie value can be parsed as signed request and it contains 'code'
550             # to exchange for access toekn.
551 1         3 return sprintf('fbsr_%d', $self->app_id);
552             }
553              
554             sub create_response {
555 53     53 1 122 my $self = shift;
556             return Facebook::OpenGraph::Response->new(+{
557             json => $self->json,
558             map {
559 53         203 $_ => shift
  318         924  
560             } qw/code message headers content req_headers req_content/,
561             });
562             }
563              
564             sub prep_param {
565 44     44 1 191 my ($self, $param_ref) = @_;
566              
567 44   50     366 $param_ref = Data::Recursive::Encode->encode_utf8($param_ref || +{});
568              
569             # /?ids=4,http://facebook-docs.oklahome.net
570 44 100       4128 if (my $ids = $param_ref->{ids}) {
571 1 50       7 $param_ref->{ids} = ref $ids ? join q{,}, @$ids : $ids;
572             }
573              
574             # mostly for /APP_ID/accounts/test-users
575 44 100       145 if (my $perms = $param_ref->{permissions}) {
576 5 100       26 $param_ref->{permissions} = ref $perms ? join q{,}, @$perms : $perms;
577             }
578              
579             # Source, file and video_file_chunk parameter contains file path.
580             # It must be an array ref to work with HTTP::Request::Common.
581 44 100       169 if (my $path = $param_ref->{source}) {
582 3 50       14 $param_ref->{source} = ref $path ? $path : [$path];
583             }
584 44 100       143 if (my $path = $param_ref->{file}) {
585 1 50       5 $param_ref->{file} = ref $path ? $path : [$path];
586             }
587 44 100       125 if (my $path = $param_ref->{video_file_chunk}) {
588 1 50       6 $param_ref->{video_file_chunk} = ref $path ? $path : [$path];
589             }
590              
591             # use Field Expansion
592 44 100       186 if (my $field_ref = $param_ref->{fields}) {
593 2         8 $param_ref->{fields} = $self->prep_fields_recursive($field_ref);
594             }
595              
596             # Using Objects: Using the Object API
597             # https://developers.facebook.com/docs/opengraph/using-objects/#objectapi
598 44         90 my $object = $param_ref->{object};
599 44 100 66     140 if ($object && ref $object eq 'HASH') {
600 1         3 $param_ref->{object} = $self->json->encode($object);
601             }
602              
603 44         106 return $param_ref;
604             }
605              
606             # Using the Graph API: Reading > Choosing Fields > Making Nested Requests
607             # https://developers.facebook.com/docs/graph-api/using-graph-api/
608             sub prep_fields_recursive {
609 4     4 1 10 my ($self, $val) = @_;
610              
611 4         10 my $ref = ref $val;
612 4 100       22 if (!$ref) {
    50          
    0          
613 3         13 return $val;
614             }
615             elsif ($ref eq 'ARRAY') {
616 1         4 return join q{,}, map { $self->prep_fields_recursive($_) } @$val;
  2         7  
617             }
618             elsif ($ref eq 'HASH') {
619 0         0 my @strs = ();
620 0         0 while (my ($k, $v) = each %$val) {
621 0         0 my $r = ref $v;
622 0 0 0     0 my $pattern = $r && $r eq 'HASH' ? '%s.%s' : '%s(%s)';
623 0         0 push @strs, sprintf($pattern, $k, $self->prep_fields_recursive($v));
624             }
625 0         0 return join q{.}, @strs;
626             }
627             }
628              
629             # Using Actions > Publishing Actions
630             # https://developers.facebook.com/docs/opengraph/using-actions/#publish
631             sub publish_action {
632 1     1 1 26 my $self = shift;
633 1         3 my $action = shift;
634 1 50       3 croak 'namespace is not set' unless $self->namespace;
635 1         4 return $self->post(sprintf('/me/%s:%s', $self->namespace, $action), @_);
636             }
637              
638             # Using Objects > Using the Object API > Images with the Object API
639             # https://developers.facebook.com/docs/opengraph/using-objects/
640             sub publish_staging_resource {
641 1     1 1 24 my $self = shift;
642 1         2 my $file = shift;
643 1         5 return $self->post('/me/staging_resources', +{file => $file}, @_);
644             }
645              
646             # Test Users: Creating
647             # https://developers.facebook.com/docs/test_users/
648             sub create_test_users {
649 2     2 1 73 my $self = shift;
650 2         4 my $settings_ref = shift;
651              
652 2 100       9 if (ref $settings_ref ne 'ARRAY') {
653 1         3 $settings_ref = [$settings_ref];
654             }
655              
656 2         6 my @settings = ();
657 2         8 my $relative_url = sprintf('/%s/accounts/test-users', $self->app_id);
658 2         7 for my $setting (@$settings_ref) {
659 3         12 push @settings, +{
660             method => 'POST',
661             relative_url => $relative_url,
662             body => $setting,
663             };
664             }
665              
666 2         10 return $self->batch(\@settings);
667             }
668              
669             # Using Objects > Using Self-Hosted Objects > Updating Objects
670             # https://developers.facebook.com/docs/opengraph/using-objects/
671             sub check_object {
672 6     6 1 251 my ($self, $target) = @_;
673 6         31 my $param_ref = +{
674             id => $target, # $target is object url or open graph object id
675             scrape => 'true',
676             };
677 6         16 return $self->post(q{}, $param_ref);
678             }
679              
680             1;
681             __END__