File Coverage

blib/lib/Facebook/OpenGraph.pm
Criterion Covered Total %
statement 253 260 97.3
branch 86 108 79.6
condition 40 54 74.0
subroutine 58 58 100.0
pod 42 43 97.6
total 479 523 91.5


line stmt bran cond sub pod time code
1             package Facebook::OpenGraph;
2 33     33   2395800 use strict;
  33         217  
  33         843  
3 33     33   160 use warnings;
  33         55  
  33         988  
4 33     33   707 use 5.008001;
  33         103  
5              
6 33     33   12012 use Facebook::OpenGraph::Response;
  33         86  
  33         942  
7 33     33   14291 use HTTP::Request::Common;
  33         673049  
  33         2096  
8 33     33   239 use URI;
  33         70  
  33         722  
9 33     33   5444 use Furl::HTTP;
  33         165806  
  33         1017  
10 33     33   13652 use Data::Recursive::Encode;
  33         350453  
  33         1173  
11 33     33   220 use JSON 2 ();
  33         714  
  33         720  
12 33     33   156 use Carp qw(croak);
  33         61  
  33         1507  
13 33     33   14839 use Digest::SHA qw(hmac_sha256 hmac_sha256_hex);
  33         86648  
  33         2518  
14 33     33   12760 use MIME::Base64::URLSafe qw(urlsafe_b64decode);
  33         39046  
  33         1800  
15 33     33   208 use Scalar::Util qw(blessed);
  33         66  
  33         98472  
16              
17             our $VERSION = '1.25';
18              
19             sub new {
20 64     64 1 141726 my $class = shift;
21 64   100     278 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 64   100     2315 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 220 sub app_id { shift->{app_id} }
44 29     29 1 208 sub secret { shift->{secret} }
45 40     40 1 772 sub ua { shift->{ua} }
46 4     4 1 21 sub namespace { shift->{namespace} }
47 82     82 1 2553 sub access_token { shift->{access_token} }
48 16     16 1 79 sub redirect_uri { shift->{redirect_uri} }
49 17     17 1 349 sub batch_limit { shift->{batch_limit} }
50 57     57 1 223 sub is_beta { shift->{is_beta} }
51 66     66 1 1283 sub json { shift->{json} }
52 40     40 1 136 sub use_appsecret_proof { shift->{use_appsecret_proof} }
53 2     2 1 8 sub use_post_method { shift->{use_post_method} }
54 61     61 1 248 sub version { shift->{version} }
55              
56             sub uri {
57 44     44 1 15248 my $self = shift;
58              
59 44 100       131 my $base = $self->is_beta ? 'https://graph.beta.facebook.com/'
60             : 'https://graph.facebook.com/'
61             ;
62              
63 44         169 return $self->_uri($base, @_);
64             }
65              
66             sub video_uri {
67 7     7 1 12279 my $self = shift;
68              
69 7 100       17 my $base = $self->is_beta ? 'https://graph-video.beta.facebook.com/'
70             : 'https://graph-video.facebook.com/'
71             ;
72              
73 7         19 return $self->_uri($base, @_);
74             }
75              
76             sub site_uri {
77 4     4 1 12 my $self = shift;
78              
79 4 100       9 my $base = $self->is_beta ? 'https://www.beta.facebook.com/'
80             : 'https://www.facebook.com/'
81             ;
82              
83 4         11 return $self->_uri($base, @_);
84             }
85              
86             sub _uri {
87 55     55   153 my ($self, $base, $path, $param_ref) = @_;
88 55   100     341 my $uri = URI->new_abs($path || '/', $base);
89             $uri->query_form(+{
90             $uri->query_form, # when given $path is like /foo?bar=bazz
91 55 100       186491 %{ $param_ref || +{} }, # additional query parameter
  55         1232  
92             });
93              
94 55         3094 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 125 my ($self, $signed_request) = @_;
101 5 50       10 croak 'signed_request is not given' unless $signed_request;
102 5 100       10 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         17 my ($enc_sig, $payload) = split(m{ \. }xms, $signed_request);
107              
108             # "2. Decode the first part - the encoded signature - from base64url"
109 4         11 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         67 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       63 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       19 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 289 my ($self, $param_ref) = @_;
138 6   100     17 $param_ref ||= +{};
139 6 100 66     12 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       14 if (my $scope_ref = ref $param_ref->{scope}) {
145 4 100       16 croak 'scope must be string or array ref' unless $scope_ref eq 'ARRAY';
146 3         5 $param_ref->{scope} = join q{,}, @{ $param_ref->{scope} };
  3         11  
147             }
148              
149             # "The URL to redirect to after a button is clicked or tapped in the
150             # dialog."
151 4         8 $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         6 $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     15 $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         11 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         11 $uri->path( $self->gen_versioned_path($uri->path) );
173              
174 4         116 return $uri->as_string;
175             }
176              
177             sub set_access_token {
178 4     4 1 15 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 153 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         13 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 49 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         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 179 my ($self, $cookie_value) = @_;
211              
212 4 100       21 croak 'cookie value is not given' unless $cookie_value;
213              
214 3         6 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       18 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         5 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 119 my ($self, $short_term_token) = @_;
240              
241 3 50       6 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         7 return $self->_get_token($query_ref);
248             }
249              
250             sub _get_token {
251 10     10   23 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         25 $param_ref = +{
256             %$param_ref,
257             client_id => $self->app_id,
258             client_secret => $self->secret,
259             };
260              
261 6         23 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         30 my $token_ref = +{ URI->new("?$res_content")->query_form };
280             croak "can't get access_token properly: $res_content"
281 5 50       681 unless $token_ref->{access_token};
282              
283 5         58 return $token_ref;
284             }
285              
286             sub get {
287 10     10 1 281 return shift->request('GET', @_)->as_hashref;
288             }
289              
290             sub post {
291 19     19 1 131 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 16 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 44 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         5 my $header = ['IF-None-Match' => $etag];
312 2         8 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 22 my ($self, $paths_ref) = @_;
319              
320             my @queries = map {
321 1         2 +{
322 2         8 method => 'GET',
323             relative_url => $_,
324             }
325             } @$paths_ref;
326              
327 1         3 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 254 my $self = shift;
334              
335 6         21 my $responses_ref = $self->batch_fast(@_);
336              
337             # Devide response content and create response objects that correspond to
338             # each request
339 5         12 my @data = ();
340 5         13 for my $r (@$responses_ref) {
341 7         14 for my $res_ref (@$r) {
342             my $response = $self->create_response(
343             $res_ref->{code},
344             $res_ref->{message},
345 58         124 [ map { $_->{name} => $_->{value} } @{ $res_ref->{headers} } ],
  13         31  
346             $res_ref->{body},
347 13         38 );
348 13 50       53 croak $response->error_string unless $response->is_success;
349              
350 13         61 push @data, $response->as_hashref;
351             }
352             }
353              
354 5         40 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         16 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       30 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         13 my @responses = ();
369 5         19 while(my @queries = splice @$batch, 0, $self->batch_limit) {
370              
371 7         18 for my $q (@queries) {
372 13 50 66     56 if ($q->{method} eq 'POST' && $q->{body}) {
373 3         8 my $body_ref = $self->prep_param($q->{body});
374 3         13 my $uri = URI->new;
375 3         4300 $uri->query_form(%$body_ref);
376 3         375 $q->{body} = $uri->query;
377             }
378             }
379              
380 7         49 my @req = (
381             '/',
382             +{
383             access_token => $self->access_token,
384             batch => $self->json->encode(\@queries),
385             },
386             @_,
387             );
388              
389 7         26 push @responses, $self->post(@req);
390             }
391              
392 5         16 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 24 my $self = shift;
399 2         4 my $query = shift;
400 2         9 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         3 return $self->fql($self->json->encode($batch), @_);
409             }
410              
411             sub request {
412 38     38 1 128 my ($self, $method, $uri, $param_ref, $headers) = @_;
413              
414 38         112 $method = uc $method;
415 38 50 33     249 $uri = $self->uri($uri) unless blessed($uri) && $uri->isa('URI');
416 38         129 $uri->path( $self->gen_versioned_path($uri->path) );
417             $param_ref = $self->prep_param(+{
418             $uri->query_form(+{}),
419 38 100       1319 %{$param_ref || +{}},
  38         1702  
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 38 100       154 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 38 100       122 if ($self->{use_post_method}) {
435 2         4 $param_ref->{method} = $method;
436 2         3 $method = 'POST';
437             }
438              
439 38   100     197 $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 38 100       124 if ($self->access_token) {
446 17         47 push @$headers, (
447             'Authorization',
448             sprintf('OAuth %s', $self->access_token),
449             );
450             }
451              
452 38         80 my $content = q{};
453 38 100       122 if ($method eq 'POST') {
454 21 100 100     103 if ($param_ref->{source} || $param_ref->{file}) {
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 3 100       11 if ($uri->path =~ m{\A /.+/videos \z}xms) {
463 1         15 $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 3         115 push @$headers, (Content_Type => 'form-data');
469              
470             # Furl::HTTP document says we can use multipart/form-data with
471             # HTTP::Request::Common.
472 3         19 my $req = POST $uri, @$headers, Content => [%$param_ref];
473 3         53382 $content = $req->content;
474 3         98 my $req_header = $req->headers;
475             $headers = +[
476             map {
477 3         29 my $k = $_;
  9         135  
478 9         22 map { ( $k => $_ ) } $req_header->header($k);
  9         269  
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         32 $content = $param_ref;
487             }
488             }
489             else {
490 17         61 $uri->query_form($param_ref);
491             }
492              
493 38         2061 my ($res_minor_version, @res_elms) = $self->ua->request(
494             method => $method,
495             url => $uri,
496             headers => $headers,
497             content => $content,
498             );
499              
500 38         72391 my $res = $self->create_response(@res_elms);
501              
502             # return F::OG::Response object on success
503 38 100       195 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       14 if ($res->req_headers) {
509 0         0 $msg .= "\n" . $res->req_headers . $res->req_content;
510             }
511 4         86 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 51 my $self = shift;
518 2 50       5 croak 'app secret must be set' unless $self->secret;
519 2 50       5 croak 'access_token must be set' unless $self->access_token;
520 2         7 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 54     54 0 630 my ($self, $path) = @_;
527              
528 54 100       157 $path = '/' unless $path;
529              
530 54 100 100     154 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 54         202 return $path;
538             }
539              
540             sub js_cookie_name {
541 2     2 1 75 my $self = shift;
542 2 100       5 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         2 return sprintf('fbsr_%d', $self->app_id);
552             }
553              
554             sub create_response {
555 51     51 1 112 my $self = shift;
556             return Facebook::OpenGraph::Response->new(+{
557             json => $self->json,
558             map {
559 51         199 $_ => shift
  306         827  
560             } qw/code message headers content req_headers req_content/,
561             });
562             }
563              
564             sub prep_param {
565 42     42 1 179 my ($self, $param_ref) = @_;
566              
567 42   50     336 $param_ref = Data::Recursive::Encode->encode_utf8($param_ref || +{});
568              
569             # /?ids=4,http://facebook-docs.oklahome.net
570 42 100       3832 if (my $ids = $param_ref->{ids}) {
571 1 50       6 $param_ref->{ids} = ref $ids ? join q{,}, @$ids : $ids;
572             }
573              
574             # mostly for /APP_ID/accounts/test-users
575 42 100       133 if (my $perms = $param_ref->{permissions}) {
576 5 100       23 $param_ref->{permissions} = ref $perms ? join q{,}, @$perms : $perms;
577             }
578              
579             # Source and file parameter contains file path.
580             # It must be an array ref to work with HTTP::Request::Common.
581 42 100       130 if (my $path = $param_ref->{source}) {
582 3 50       14 $param_ref->{source} = ref $path ? $path : [$path];
583             }
584 42 100       120 if (my $path = $param_ref->{file}) {
585 1 50       7 $param_ref->{file} = ref $path ? $path : [$path];
586             }
587              
588             # use Field Expansion
589 42 100       107 if (my $field_ref = $param_ref->{fields}) {
590 2         7 $param_ref->{fields} = $self->prep_fields_recursive($field_ref);
591             }
592              
593             # Using Objects: Using the Object API
594             # https://developers.facebook.com/docs/opengraph/using-objects/#objectapi
595 42         86 my $object = $param_ref->{object};
596 42 100 66     155 if ($object && ref $object eq 'HASH') {
597 1         3 $param_ref->{object} = $self->json->encode($object);
598             }
599              
600 42         91 return $param_ref;
601             }
602              
603             # Using the Graph API: Reading > Choosing Fields > Making Nested Requests
604             # https://developers.facebook.com/docs/graph-api/using-graph-api/
605             sub prep_fields_recursive {
606 4     4 1 10 my ($self, $val) = @_;
607              
608 4         10 my $ref = ref $val;
609 4 100       9 if (!$ref) {
    50          
    0          
610 3         10 return $val;
611             }
612             elsif ($ref eq 'ARRAY') {
613 1         3 return join q{,}, map { $self->prep_fields_recursive($_) } @$val;
  2         5  
614             }
615             elsif ($ref eq 'HASH') {
616 0         0 my @strs = ();
617 0         0 while (my ($k, $v) = each %$val) {
618 0         0 my $r = ref $v;
619 0 0 0     0 my $pattern = $r && $r eq 'HASH' ? '%s.%s' : '%s(%s)';
620 0         0 push @strs, sprintf($pattern, $k, $self->prep_fields_recursive($v));
621             }
622 0         0 return join q{.}, @strs;
623             }
624             }
625              
626             # Using Actions > Publishing Actions
627             # https://developers.facebook.com/docs/opengraph/using-actions/#publish
628             sub publish_action {
629 1     1 1 23 my $self = shift;
630 1         2 my $action = shift;
631 1 50       4 croak 'namespace is not set' unless $self->namespace;
632 1         4 return $self->post(sprintf('/me/%s:%s', $self->namespace, $action), @_);
633             }
634              
635             # Using Objects > Using the Object API > Images with the Object API
636             # https://developers.facebook.com/docs/opengraph/using-objects/
637             sub publish_staging_resource {
638 1     1 1 24 my $self = shift;
639 1         1 my $file = shift;
640 1         5 return $self->post('/me/staging_resources', +{file => $file}, @_);
641             }
642              
643             # Test Users: Creating
644             # https://developers.facebook.com/docs/test_users/
645             sub create_test_users {
646 2     2 1 49 my $self = shift;
647 2         3 my $settings_ref = shift;
648              
649 2 100       7 if (ref $settings_ref ne 'ARRAY') {
650 1         3 $settings_ref = [$settings_ref];
651             }
652              
653 2         4 my @settings = ();
654 2         6 my $relative_url = sprintf('/%s/accounts/test-users', $self->app_id);
655 2         6 for my $setting (@$settings_ref) {
656 3         11 push @settings, +{
657             method => 'POST',
658             relative_url => $relative_url,
659             body => $setting,
660             };
661             }
662              
663 2         7 return $self->batch(\@settings);
664             }
665              
666             # Using Objects > Using Self-Hosted Objects > Updating Objects
667             # https://developers.facebook.com/docs/opengraph/using-objects/
668             sub check_object {
669 6     6 1 250 my ($self, $target) = @_;
670 6         15 my $param_ref = +{
671             id => $target, # $target is object url or open graph object id
672             scrape => 'true',
673             };
674 6         16 return $self->post(q{}, $param_ref);
675             }
676              
677             1;
678             __END__